@hasna/hooks 0.7.10 → 0.9.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 (96) hide show
  1. package/README.md +32 -6
  2. package/bin/index.js +2640 -1820
  3. package/bin/serve.js +742 -551
  4. package/dist/config.d.ts +25 -19
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +1251 -717
  7. package/dist/lib/app-home.d.ts +9 -0
  8. package/dist/lib/installer.d.ts +40 -10
  9. package/dist/lib/local-opt-in.d.ts +80 -0
  10. package/dist/lib/profiles.d.ts +10 -0
  11. package/dist/lib/registration.d.ts +75 -0
  12. package/dist/lib/registry.d.ts +14 -0
  13. package/dist/lib/resolver-types.d.ts +45 -0
  14. package/dist/lib/sync.d.ts +13 -8
  15. package/dist/lib/transport.d.ts +87 -0
  16. package/dist/openapi.d.ts +1 -1
  17. package/dist/storage.js +54 -63
  18. package/hooks/hook-affected-tests/package.json +1 -1
  19. package/hooks/hook-agent-rules-version-check/README.md +1 -1
  20. package/hooks/hook-agent-rules-version-check/package.json +1 -1
  21. package/hooks/hook-agentmessages/package.json +1 -1
  22. package/hooks/hook-announce-start/package.json +1 -1
  23. package/hooks/hook-announce-stop/package.json +1 -1
  24. package/hooks/hook-autoformat/package.json +1 -1
  25. package/hooks/hook-branchprotect/package.json +1 -1
  26. package/hooks/hook-checkbugs/package.json +1 -1
  27. package/hooks/hook-checkdocs/package.json +1 -1
  28. package/hooks/hook-checkfiles/package.json +1 -1
  29. package/hooks/hook-checklint/package.json +1 -1
  30. package/hooks/hook-checkpoint/package.json +1 -1
  31. package/hooks/hook-checksecurity/package.json +1 -1
  32. package/hooks/hook-checktasks/package.json +1 -1
  33. package/hooks/hook-checktests/package.json +1 -1
  34. package/hooks/hook-conflict-detect/package.json +1 -1
  35. package/hooks/hook-contextrefresh/package.json +1 -1
  36. package/hooks/hook-desktopnotify/package.json +1 -1
  37. package/hooks/hook-dm-inject/package.json +1 -1
  38. package/hooks/hook-envsetup/package.json +1 -1
  39. package/hooks/hook-failure-to-task/package.json +1 -1
  40. package/hooks/hook-filelock/package.json +1 -1
  41. package/hooks/hook-fleet-blockers-gate/package.json +1 -1
  42. package/hooks/hook-fleet-catchup/package.json +1 -1
  43. package/hooks/hook-gitguard/package.json +1 -1
  44. package/hooks/hook-packageage/package.json +1 -1
  45. package/hooks/hook-permissionguard/package.json +1 -1
  46. package/hooks/hook-phonenotify/package.json +1 -1
  47. package/hooks/hook-precompact/package.json +1 -1
  48. package/hooks/hook-protectfiles/package.json +1 -1
  49. package/hooks/hook-stylescheck/package.json +1 -1
  50. package/hooks/hook-trash-guard/README.md +132 -0
  51. package/hooks/hook-trash-guard/package.json +12 -0
  52. package/hooks/hook-trash-guard/src/hook.ts +1114 -0
  53. package/hooks/hook-typecheck-gate/package.json +1 -1
  54. package/package.json +7 -10
  55. package/scripts/ensure-profiles-dir.mjs +52 -1
  56. package/hooks/codewith-native-common.test.ts +0 -1935
  57. package/hooks/hook-affected-tests/tsconfig.json +0 -25
  58. package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
  59. package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
  60. package/hooks/hook-announce-start/tsconfig.json +0 -25
  61. package/hooks/hook-announce-stop/tsconfig.json +0 -25
  62. package/hooks/hook-autoformat/tsconfig.json +0 -25
  63. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  64. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  65. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  66. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  67. package/hooks/hook-checklint/tsconfig.json +0 -15
  68. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  69. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  70. package/hooks/hook-checktasks/tsconfig.json +0 -20
  71. package/hooks/hook-checktests/tsconfig.json +0 -15
  72. package/hooks/hook-conflict-detect/tsconfig.json +0 -25
  73. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  74. package/hooks/hook-desktopnotify/tsconfig.json +0 -25
  75. package/hooks/hook-dm-inject/tsconfig.json +0 -25
  76. package/hooks/hook-envsetup/tsconfig.json +0 -25
  77. package/hooks/hook-failure-to-task/tsconfig.json +0 -25
  78. package/hooks/hook-filelock/tsconfig.json +0 -25
  79. package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
  80. package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
  81. package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
  82. package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
  83. package/hooks/hook-gitguard/tsconfig.json +0 -25
  84. package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
  85. package/hooks/hook-packageage/tsconfig.json +0 -25
  86. package/hooks/hook-permissionguard/tsconfig.json +0 -25
  87. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  88. package/hooks/hook-precompact/tsconfig.json +0 -25
  89. package/hooks/hook-protectfiles/tsconfig.json +0 -25
  90. package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
  91. package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
  92. package/hooks/hook-stylescheck/tsconfig.json +0 -25
  93. package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
  94. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
  95. package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
  96. package/hooks/mention-context/src/hook.test.ts +0 -68
package/dist/index.js CHANGED
@@ -15,92 +15,77 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
18
+ // src/lib/app-home.ts
19
+ import { existsSync } from "fs";
19
20
  import { homedir } from "os";
20
- import { join } from "path";
21
- function assertApp(app) {
21
+ import { join, resolve } from "path";
22
+ import { homedir as pathsResolverHomedir } from "os";
23
+ import { join as pathsResolverJoin } from "path";
24
+ function pathsResolverAssertApp(app) {
22
25
  if (typeof app !== "string" || app.length === 0) {
23
26
  throw new TypeError("paths: app must be a non-empty string");
24
27
  }
25
- if (!APP_SLUG_RE.test(app)) {
28
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
26
29
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
27
30
  }
28
31
  }
29
- function envOf(options) {
30
- return options.env ?? process.env;
31
- }
32
- function envValue(options, kind) {
33
- const value = envOf(options)[KIND_ENV[kind]];
34
- return typeof value === "string" && value.length > 0 ? value : undefined;
35
- }
36
- function isMacOS(platform) {
37
- return platform === "darwin";
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
+ }
38
36
  }
39
- function baseDir(kind, options) {
40
- const override = envValue(options, kind);
41
- if (override)
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
42
  return override;
43
- const home = options.home ?? homedir();
43
+ const home = options.home ?? pathsResolverHomedir();
44
44
  const platform = options.platform ?? process.platform;
45
- if (isMacOS(platform)) {
45
+ if (platform === "darwin") {
46
46
  switch (kind) {
47
47
  case "config":
48
48
  case "data":
49
- return join(home, "Library", "Application Support", "Hasna");
49
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
50
50
  case "cache":
51
- return join(home, "Library", "Caches", "Hasna");
51
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
52
52
  case "state":
53
- return join(home, "Library", "Logs", "Hasna");
53
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
54
54
  }
55
55
  }
56
56
  switch (kind) {
57
57
  case "config":
58
- return join(home, ".config", "hasna");
58
+ return pathsResolverJoin(home, ".config", "hasna");
59
59
  case "data":
60
- return join(home, ".local", "share", "hasna");
60
+ return pathsResolverJoin(home, ".local", "share", "hasna");
61
61
  case "state":
62
- return join(home, ".local", "state", "hasna");
62
+ return pathsResolverJoin(home, ".local", "state", "hasna");
63
63
  case "cache":
64
- return join(home, ".cache", "hasna");
64
+ return pathsResolverJoin(home, ".cache", "hasna");
65
65
  }
66
66
  }
67
- function resolvePath(kind, options) {
68
- assertApp(options.app);
69
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
70
- return join(baseDir(kind, options), appSegment);
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
71
  }
72
72
  function dataDir(options) {
73
- return resolvePath("data", options);
73
+ return pathsResolverResolve("data", options);
74
74
  }
75
- var KIND_ENV, APP_SLUG_RE;
76
- var init_dist = __esm(() => {
77
- KIND_ENV = {
78
- config: "HASNA_CONFIG_HOME",
79
- data: "HASNA_DATA_HOME",
80
- state: "HASNA_STATE_HOME",
81
- cache: "HASNA_CACHE_HOME"
82
- };
83
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
84
- });
85
-
86
- // src/lib/app-home.ts
87
- import { existsSync } from "fs";
88
- import { homedir as homedir2 } from "os";
89
- import { join as join2, resolve } from "path";
90
75
  function getHomeDir(env = process.env) {
91
- return env.HOME || env.USERPROFILE || homedir2();
76
+ return env.HOME || env.USERPROFILE || homedir();
92
77
  }
93
78
  function getResolverDataRoot(env = process.env) {
94
79
  return dataDir({ app: "hooks", home: getHomeDir(env), env });
95
80
  }
96
81
  function getLegacyDataRoot(env = process.env) {
97
- return join2(getHomeDir(env), ".hasna", "hooks");
82
+ return join(getHomeDir(env), ".hasna", "hooks");
98
83
  }
99
84
  function adoptResolverDataRoot(resolved, env = process.env) {
100
85
  const dataOverride = env.HASNA_DATA_HOME;
101
86
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
102
87
  return true;
103
- return existsSync(join2(resolved, STORE_MARKER));
88
+ return existsSync(join(resolved, STORE_MARKER));
104
89
  }
105
90
  function getExplicitDataDir(env = process.env) {
106
91
  const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
@@ -128,7 +113,7 @@ function getReportedDbPath(env = process.env) {
128
113
  const explicit = getExplicitDbPath(env);
129
114
  if (explicit)
130
115
  return explicit;
131
- return join2(getEffectiveDataRoot(env), "hooks.db");
116
+ return join(getEffectiveDataRoot(env), "hooks.db");
132
117
  }
133
118
  function getExplicitDbPath(env = process.env) {
134
119
  const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
@@ -136,9 +121,15 @@ function getExplicitDbPath(env = process.env) {
136
121
  return db.trim();
137
122
  return;
138
123
  }
139
- var STORE_MARKER = "hooks.db";
124
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, STORE_MARKER = "hooks.db";
140
125
  var init_app_home = __esm(() => {
141
- init_dist();
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]+)*$/;
142
133
  });
143
134
 
144
135
  // src/db/schema.ts
@@ -336,8 +327,8 @@ var init_migrations = __esm(() => {
336
327
 
337
328
  // src/db/legacy-import.ts
338
329
  import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
339
- import { join as join3 } from "path";
340
- import { homedir as homedir3 } from "os";
330
+ import { join as join2 } from "path";
331
+ import { homedir as homedir2 } from "os";
341
332
  function ensureMetaTable(db) {
342
333
  db.exec(`
343
334
  CREATE TABLE IF NOT EXISTS _meta (
@@ -411,25 +402,25 @@ function importErrorsLog(db, filePath) {
411
402
  } catch {}
412
403
  return count;
413
404
  }
414
- function runLegacyImport(db, homeDir = homedir3()) {
405
+ function runLegacyImport(db, homeDir = homedir2()) {
415
406
  try {
416
407
  if (isAlreadyDone(db))
417
408
  return;
418
409
  let total = 0;
419
- const claudeProjectsDir = join3(homeDir, ".claude", "projects");
410
+ const claudeProjectsDir = join2(homeDir, ".claude", "projects");
420
411
  if (existsSync2(claudeProjectsDir)) {
421
412
  try {
422
413
  const projectDirs = readdirSync(claudeProjectsDir);
423
414
  for (const dir of projectDirs) {
424
- const projectDir = join3(claudeProjectsDir, dir);
415
+ const projectDir = join2(claudeProjectsDir, dir);
425
416
  try {
426
417
  const files = readdirSync(projectDir);
427
418
  for (const file of files) {
428
419
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
429
- total += importJsonlFile(db, join3(projectDir, file));
420
+ total += importJsonlFile(db, join2(projectDir, file));
430
421
  }
431
422
  if (file === "errors.log") {
432
- total += importErrorsLog(db, join3(projectDir, file));
423
+ total += importErrorsLog(db, join2(projectDir, file));
433
424
  }
434
425
  }
435
426
  } catch {}
@@ -466,10 +457,10 @@ function runRetention(db, days) {
466
457
  // src/db/index.ts
467
458
  import { Database } from "bun:sqlite";
468
459
  import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
469
- import { join as join4 } from "path";
460
+ import { join as join3 } from "path";
470
461
  function resolveDataDir() {
471
462
  const effective = getEffectiveDataRoot();
472
- const oldDir = join4(getHomeDir(), ".hooks");
463
+ const oldDir = join3(getHomeDir(), ".hooks");
473
464
  if (!existsSync3(effective) && existsSync3(oldDir)) {
474
465
  mkdirSync(effective, { recursive: true });
475
466
  cpSync(oldDir, effective, { recursive: true });
@@ -481,7 +472,7 @@ function getDbPath() {
481
472
  if (explicitDb)
482
473
  return explicitDb;
483
474
  const dataDir2 = resolveDataDir();
484
- return join4(dataDir2, "hooks.db");
475
+ return join3(dataDir2, "hooks.db");
485
476
  }
486
477
  function ensureDir(dbPath) {
487
478
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
@@ -568,8 +559,8 @@ var init_redact = __esm(() => {
568
559
  /AKIA[0-9A-Z]{16}/g,
569
560
  /ASIA[0-9A-Z]{16}/g,
570
561
  /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
571
- /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
572
- /-----BEGIN OPENSSH PRIVATE KEY-----/g,
562
+ /-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
563
+ /-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
573
564
  /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
574
565
  /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
575
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
@@ -4542,550 +4533,165 @@ var init_zod = __esm(() => {
4542
4533
  init_external();
4543
4534
  });
4544
4535
 
4545
- // ../contracts/dist/client/transport.js
4546
- import { readFileSync as readFileSync2, statSync } from "fs";
4547
- import { createRequire } from "module";
4548
- import { join as join5 } from "path";
4549
- function envToken(name) {
4550
- return name.toUpperCase().replace(/-/g, "_");
4551
- }
4552
- function clientTransportEnvKeys(name) {
4553
- const envSegment = envToken(name);
4554
- return {
4555
- apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
4556
- apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
4557
- };
4536
+ // src/config.ts
4537
+ import { join as join4 } from "path";
4538
+ function getHooksDataDir() {
4539
+ return getEffectiveDataRoot();
4558
4540
  }
4559
- function credentialOverrideEnvKey(name) {
4560
- return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4541
+ function getCustomHooksDir() {
4542
+ return join4(getHooksDataDir(), "hooks");
4561
4543
  }
4562
- function credentialPointerEnvKey(name) {
4563
- return `HASNA_${envToken(name)}_API_KEY_REF`;
4544
+ function getLockPath() {
4545
+ const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4546
+ if (explicit)
4547
+ return explicit;
4548
+ return join4(getHooksDataDir(), "hooks.lock");
4564
4549
  }
4565
- function homeDir(env) {
4566
- const home = env.HOME?.trim();
4567
- return home ? home : null;
4550
+ var init_config = __esm(() => {
4551
+ init_app_home();
4552
+ });
4553
+
4554
+ // src/lib/semver.ts
4555
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4556
+ var init_semver = __esm(() => {
4557
+ SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4558
+ });
4559
+
4560
+ // src/lib/manifest.ts
4561
+ var exports_manifest = {};
4562
+ __export(exports_manifest, {
4563
+ writeCustomHook: () => writeCustomHook,
4564
+ shortManifestName: () => shortManifestName,
4565
+ scriptRelFor: () => scriptRelFor,
4566
+ resolveScript: () => resolveScript,
4567
+ readCustomManifest: () => readCustomManifest,
4568
+ parseManifest: () => parseManifest,
4569
+ normalizeManifestName: () => normalizeManifestName,
4570
+ manifestSchema: () => manifestSchema,
4571
+ listCustomHooks: () => listCustomHooks,
4572
+ isInlineScript: () => isInlineScript,
4573
+ customHookDir: () => customHookDir,
4574
+ assertContained: () => assertContained,
4575
+ ScriptContainmentError: () => ScriptContainmentError,
4576
+ HOOK_NAME_RE: () => HOOK_NAME_RE
4577
+ });
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";
4580
+ function parseManifest(json) {
4581
+ const parsed = JSON.parse(json);
4582
+ return manifestSchema.parse(parsed);
4568
4583
  }
4569
- function credentialDiskSourceList(name, env, profile = null) {
4570
- const home = homeDir(env);
4571
- if (!home || !SAFE_APP_SLUG.test(name))
4572
- return [];
4573
- const stem = profile ? `${name}.${profile}` : name;
4574
- const configStem = profile ? `${name}-${profile}` : name;
4575
- return [
4576
- {
4577
- path: join5(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4578
- tier: "fleet-env",
4579
- deprecated: false
4580
- },
4581
- {
4582
- path: join5(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
4583
- tier: "legacy-cloud",
4584
- deprecated: true
4585
- },
4586
- {
4587
- path: join5(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
4588
- tier: "config",
4589
- deprecated: false
4590
- },
4591
- {
4592
- path: join5(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
4593
- tier: "config-legacy",
4594
- deprecated: true
4595
- }
4596
- ];
4584
+ function normalizeManifestName(name) {
4585
+ return name.startsWith("hook-") ? name : `hook-${name}`;
4597
4586
  }
4598
- function credentialDiskSources(name, env) {
4599
- return credentialDiskSourceList(name, env, null).map((s) => s.path);
4587
+ function shortManifestName(name) {
4588
+ return name.startsWith("hook-") ? name.slice("hook-".length) : name;
4600
4589
  }
4601
- function profileDiskSources(name, env, profile) {
4602
- return credentialDiskSourceList(name, env, profile).map((s) => s.path);
4590
+ function isWithin(child, parent) {
4591
+ return child === parent || child.startsWith(parent + sep);
4603
4592
  }
4604
- function parseEnvFile(text) {
4605
- const values = new Map;
4606
- for (const rawLine of text.split(/\r?\n/)) {
4607
- const line = rawLine.trim();
4608
- if (line.length === 0 || line.startsWith("#"))
4609
- continue;
4610
- const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
4611
- const equals = withoutExport.indexOf("=");
4612
- if (equals <= 0)
4613
- continue;
4614
- const key = withoutExport.slice(0, equals).trim();
4615
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
4616
- continue;
4617
- let value = withoutExport.slice(equals + 1).trim();
4618
- const quote = value[0];
4619
- if (quote === '"' || quote === "'") {
4620
- if (value.length < 2 || !value.endsWith(quote))
4621
- continue;
4622
- value = value.slice(1, -1);
4623
- }
4624
- if (value.length === 0)
4625
- continue;
4626
- values.set(key, value);
4593
+ function assertContainedLexical(target, root, raw) {
4594
+ const lexical = resolve2(target);
4595
+ const rootLexical = resolve2(root);
4596
+ if (!isWithin(lexical, rootLexical)) {
4597
+ throw new ScriptContainmentError(target, root, raw);
4627
4598
  }
4628
- return values;
4599
+ return lexical;
4629
4600
  }
4630
- function readAppConfigFile(path) {
4631
- let text;
4632
- try {
4633
- const stats = statSync(path);
4634
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
4635
- return null;
4636
- text = readFileSync2(path, "utf8");
4637
- } catch {
4638
- return null;
4601
+ function realpathNearest(path) {
4602
+ const tail = [];
4603
+ let cur = path;
4604
+ while (!existsSync4(cur)) {
4605
+ const parent = dirname(cur);
4606
+ if (parent === cur)
4607
+ break;
4608
+ tail.unshift(basename(cur));
4609
+ cur = parent;
4639
4610
  }
4640
- return parseEnvFile(text);
4611
+ return join5(realpathSync(cur), ...tail);
4641
4612
  }
4642
- function readCredentialFile(path, apiKeyKeys) {
4643
- const values = readAppConfigFile(path);
4644
- if (!values)
4645
- return null;
4646
- for (const key of apiKeyKeys) {
4647
- const value = values.get(key)?.trim();
4648
- if (value)
4649
- return value;
4613
+ function assertContainedReal(root, target, raw) {
4614
+ const rootReal = realpathSync(root);
4615
+ const targetReal = realpathNearest(target);
4616
+ if (!isWithin(targetReal, rootReal)) {
4617
+ throw new ScriptContainmentError(target, root, raw);
4650
4618
  }
4651
- return null;
4652
4619
  }
4653
- function assertUsableCredential(appName, source, value) {
4654
- if (VAULT_POINTER_SHAPE.test(value)) {
4655
- 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]);
4620
+ function assertContained(target, root, raw) {
4621
+ const lexical = assertContainedLexical(target, root, raw);
4622
+ assertContainedReal(root, lexical, raw);
4623
+ return lexical;
4624
+ }
4625
+ function isInlineScript(manifest) {
4626
+ return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
4627
+ `);
4628
+ }
4629
+ function scriptRelFor(manifest) {
4630
+ if (isInlineScript(manifest)) {
4631
+ const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
4632
+ return `script${ext}`;
4656
4633
  }
4657
- if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4658
- return;
4659
- 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]);
4634
+ const rel = normalize(manifest.script);
4635
+ if (isAbsolute(rel)) {
4636
+ throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
4637
+ }
4638
+ return rel;
4660
4639
  }
4661
- function sealCredential(fields) {
4662
- const { apiKey } = fields;
4663
- const visible = {
4664
- tier: fields.tier,
4665
- source: fields.source,
4666
- deliberate: fields.deliberate,
4667
- deprecated: fields.deprecated,
4668
- diskCandidates: Object.freeze([...fields.diskCandidates]),
4669
- warning: fields.warning
4670
- };
4671
- const sealed = { ...visible };
4672
- Object.defineProperty(sealed, "apiKey", {
4673
- value: apiKey,
4674
- enumerable: false,
4675
- writable: false,
4676
- configurable: false
4677
- });
4678
- if (fields.pointerVaultKey !== undefined) {
4679
- Object.defineProperty(sealed, "pointerVaultKey", {
4680
- value: fields.pointerVaultKey,
4681
- enumerable: false,
4682
- writable: false,
4683
- configurable: false
4684
- });
4640
+ function resolveScript(manifest, manifestDir) {
4641
+ if (isInlineScript(manifest))
4642
+ return { path: scriptRelFor(manifest), content: manifest.script };
4643
+ const rel = scriptRelFor(manifest);
4644
+ const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
4645
+ if (!existsSync4(resolved)) {
4646
+ throw new Error(`manifest script file not found: ${resolved}`);
4685
4647
  }
4686
- Object.defineProperty(sealed, INSPECT_CUSTOM, {
4687
- value: () => ({ ...visible, apiKey: "[redacted]" }),
4688
- enumerable: false,
4689
- writable: false,
4690
- configurable: false
4691
- });
4692
- Object.defineProperty(sealed, CREDENTIAL_SEAL, {
4693
- value: true,
4694
- enumerable: false,
4695
- writable: false,
4696
- configurable: false
4697
- });
4698
- return Object.freeze(sealed);
4648
+ return { path: rel, content: readFileSync2(resolved, "utf-8") };
4699
4649
  }
4700
- function firstEnvValue(env, keys) {
4701
- for (const key of keys) {
4702
- const value = env[key]?.trim();
4703
- if (value)
4704
- return { key, value };
4650
+ function customHookDir(name) {
4651
+ return join5(getCustomHooksDir(), shortManifestName(name));
4652
+ }
4653
+ function readCustomManifest(name) {
4654
+ const dir = customHookDir(name);
4655
+ const manifestPath = join5(dir, "manifest.json");
4656
+ if (!existsSync4(manifestPath))
4657
+ return;
4658
+ try {
4659
+ const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4660
+ const script = resolveScript(manifest, dir);
4661
+ const scriptIsInline = isInlineScript(manifest);
4662
+ return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
4663
+ } catch (err) {
4664
+ if (err instanceof ScriptContainmentError)
4665
+ throw err;
4666
+ return;
4705
4667
  }
4706
- return null;
4707
4668
  }
4708
- function deprecationNotified() {
4709
- const host = globalThis;
4710
- const existing = host[DEPRECATION_REGISTRY];
4711
- if (existing instanceof Set)
4712
- return existing;
4713
- const created = new Set;
4714
- host[DEPRECATION_REGISTRY] = created;
4715
- return created;
4716
- }
4717
- function defaultDeprecationSink(message) {
4718
- if (typeof process !== "undefined" && process.stderr) {
4719
- process.stderr.write(`${message}
4720
- `);
4669
+ function listCustomHooks() {
4670
+ const dir = getCustomHooksDir();
4671
+ if (!existsSync4(dir))
4672
+ return [];
4673
+ const results = [];
4674
+ for (const entry of readdirSafe(dir)) {
4675
+ const parsed = readCustomManifest(entry);
4676
+ if (parsed)
4677
+ results.push(parsed);
4721
4678
  }
4722
- }
4723
- function resolveCredential(name, env, options = {}) {
4724
- const { apiKeyKeys } = clientTransportEnvKeys(name);
4725
- const diskPaths = credentialDiskSources(name, env);
4726
- const explicitKey = options.apiKey?.trim();
4727
- if (explicitKey) {
4728
- assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
4729
- return sealCredential({
4730
- apiKey: explicitKey,
4731
- tier: "argument",
4732
- source: "explicit apiKey argument",
4733
- deliberate: true,
4734
- deprecated: false,
4735
- diskCandidates: diskPaths,
4736
- warning: null
4737
- });
4738
- }
4739
- const overrideKeyName = credentialOverrideEnvKey(name);
4740
- const overrideRaw = env[overrideKeyName];
4741
- if (overrideRaw !== undefined) {
4742
- const override = overrideRaw.trim();
4743
- if (!override) {
4744
- 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]);
4745
- }
4746
- assertUsableCredential(name, overrideKeyName, override);
4747
- return sealCredential({
4748
- apiKey: override,
4749
- tier: "override",
4750
- source: overrideKeyName,
4751
- deliberate: true,
4752
- deprecated: false,
4753
- diskCandidates: diskPaths,
4754
- warning: null
4755
- });
4756
- }
4757
- const pointerKeyName = credentialPointerEnvKey(name);
4758
- const pointerRaw = env[pointerKeyName];
4759
- if (pointerRaw !== undefined) {
4760
- const pointer = pointerRaw.trim();
4761
- if (!pointer) {
4762
- 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]);
4763
- }
4764
- if (!VAULT_POINTER_SHAPE.test(pointer)) {
4765
- 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]);
4766
- }
4767
- return sealCredential({
4768
- apiKey: "",
4769
- pointerVaultKey: pointer,
4770
- tier: "pointer",
4771
- source: pointerKeyName,
4772
- deliberate: true,
4773
- deprecated: false,
4774
- diskCandidates: diskPaths,
4775
- warning: null
4776
- });
4777
- }
4778
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4779
- if (profile) {
4780
- const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
4781
- if (!SAFE_PROFILE.test(profile)) {
4782
- throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
4783
- }
4784
- const paths = profileDiskSources(name, env, profile);
4785
- for (const path of paths) {
4786
- const value = readCredentialFile(path, apiKeyKeys);
4787
- if (value) {
4788
- assertUsableCredential(name, path, value);
4789
- return sealCredential({
4790
- apiKey: value,
4791
- tier: "profile",
4792
- source: path,
4793
- deliberate: true,
4794
- deprecated: false,
4795
- diskCandidates: paths,
4796
- warning: null
4797
- });
4798
- }
4799
- }
4800
- 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);
4801
- }
4802
- const diskSourceList = credentialDiskSourceList(name, env, null);
4803
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4804
- if (diskHits.length > 0) {
4805
- const winner = diskHits[0];
4806
- assertUsableCredential(name, winner.src.path, winner.value);
4807
- const divergentSources = [
4808
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
4809
- ...(() => {
4810
- const legacyHit = firstEnvValue(env, apiKeyKeys);
4811
- return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4812
- })()
4813
- ];
4814
- 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;
4815
- let deprecated = winner.src.deprecated;
4816
- let finalWarning = warning;
4817
- if (winner.src.deprecated) {
4818
- deprecated = true;
4819
- const sink = options.onDeprecation ?? defaultDeprecationSink;
4820
- const notified = deprecationNotified();
4821
- const noticeKey = `${name}:${winner.src.path}`;
4822
- if (!notified.has(noticeKey)) {
4823
- notified.add(noticeKey);
4824
- const target = diskSourceList[0]?.path ?? "<none>";
4825
- const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
4826
- sink(message);
4827
- }
4828
- finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
4829
- }
4830
- return sealCredential({
4831
- apiKey: winner.value,
4832
- tier: winner.src.tier,
4833
- source: winner.src.path,
4834
- deliberate: false,
4835
- deprecated,
4836
- diskCandidates: diskPaths,
4837
- warning: finalWarning
4838
- });
4839
- }
4840
- const legacy = firstEnvValue(env, apiKeyKeys);
4841
- if (legacy) {
4842
- assertUsableCredential(name, legacy.key, legacy.value);
4843
- 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.`;
4844
- 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}`;
4845
- const sink = options.onDeprecation ?? defaultDeprecationSink;
4846
- const notified = deprecationNotified();
4847
- if (!notified.has(name)) {
4848
- notified.add(name);
4849
- sink(message);
4850
- }
4851
- return sealCredential({
4852
- apiKey: legacy.value,
4853
- tier: "legacy-env",
4854
- source: legacy.key,
4855
- deliberate: false,
4856
- deprecated: true,
4857
- diskCandidates: diskPaths,
4858
- warning: message
4859
- });
4860
- }
4861
- return null;
4862
- }
4863
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "fleet-env", LEGACY_CLOUD_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
4864
- var init_transport = __esm(() => {
4865
- CredentialResolutionError = class CredentialResolutionError extends Error {
4866
- appName;
4867
- attempted;
4868
- constructor(appName, message, attempted) {
4869
- super(message);
4870
- this.name = "CredentialResolutionError";
4871
- this.appName = appName;
4872
- this.attempted = attempted;
4873
- }
4874
- };
4875
- MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4876
- SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4877
- SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4878
- ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4879
- VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
4880
- INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
4881
- CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
4882
- DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
4883
- SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
4884
- requireSecretsSdk = createRequire(import.meta.url);
4885
- IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4886
- AUTHORITY_OVERRIDE_HEADERS = new Set([
4887
- "host",
4888
- ":authority",
4889
- "forwarded",
4890
- "x-forwarded-host",
4891
- "x-original-host"
4892
- ]);
4893
- });
4894
-
4895
- // src/config.ts
4896
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
4897
- import { join as join6 } from "path";
4898
- function getHooksDataDir() {
4899
- return getEffectiveDataRoot();
4900
- }
4901
- function getCustomHooksDir() {
4902
- return join6(getHooksDataDir(), "hooks");
4903
- }
4904
- function getLockPath() {
4905
- const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4906
- if (explicit)
4907
- return explicit;
4908
- return join6(getHooksDataDir(), "hooks.lock");
4909
- }
4910
- function getConfigPath() {
4911
- const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
4912
- if (explicit)
4913
- return explicit;
4914
- return join6(getHooksDataDir(), "config.json");
4915
- }
4916
- function readConfig() {
4917
- try {
4918
- const path = getConfigPath();
4919
- if (!existsSync4(path))
4920
- return {};
4921
- return JSON.parse(readFileSync3(path, "utf-8"));
4922
- } catch {
4923
- return {};
4924
- }
4925
- }
4926
- function writeConfig(config) {
4927
- const path = getConfigPath();
4928
- mkdirSync2(join6(path, ".."), { recursive: true });
4929
- writeFileSync(path, JSON.stringify(config, null, 2) + `
4930
- `, "utf-8");
4931
- return path;
4932
- }
4933
- function resolveApiUrl() {
4934
- const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
4935
- if (envUrl)
4936
- return envUrl.replace(/\/+$/, "");
4937
- const cfg = readConfig();
4938
- return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4939
- }
4940
- function resolveApiKey() {
4941
- return resolveCredential("hooks", process.env)?.apiKey;
4942
- }
4943
- var init_config = __esm(() => {
4944
- init_transport();
4945
- init_app_home();
4946
- });
4947
-
4948
- // src/lib/semver.ts
4949
- var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4950
- var init_semver = __esm(() => {
4951
- SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4952
- });
4953
-
4954
- // src/lib/manifest.ts
4955
- var exports_manifest = {};
4956
- __export(exports_manifest, {
4957
- writeCustomHook: () => writeCustomHook,
4958
- shortManifestName: () => shortManifestName,
4959
- scriptRelFor: () => scriptRelFor,
4960
- resolveScript: () => resolveScript,
4961
- readCustomManifest: () => readCustomManifest,
4962
- parseManifest: () => parseManifest,
4963
- normalizeManifestName: () => normalizeManifestName,
4964
- manifestSchema: () => manifestSchema,
4965
- listCustomHooks: () => listCustomHooks,
4966
- isInlineScript: () => isInlineScript,
4967
- customHookDir: () => customHookDir,
4968
- assertContained: () => assertContained,
4969
- ScriptContainmentError: () => ScriptContainmentError,
4970
- HOOK_NAME_RE: () => HOOK_NAME_RE
4971
- });
4972
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4973
- import { basename, dirname, isAbsolute, join as join7, normalize, resolve as resolve2, sep } from "path";
4974
- function parseManifest(json) {
4975
- const parsed = JSON.parse(json);
4976
- return manifestSchema.parse(parsed);
4977
- }
4978
- function normalizeManifestName(name) {
4979
- return name.startsWith("hook-") ? name : `hook-${name}`;
4980
- }
4981
- function shortManifestName(name) {
4982
- return name.startsWith("hook-") ? name.slice("hook-".length) : name;
4983
- }
4984
- function isWithin(child, parent) {
4985
- return child === parent || child.startsWith(parent + sep);
4986
- }
4987
- function assertContainedLexical(target, root, raw) {
4988
- const lexical = resolve2(target);
4989
- const rootLexical = resolve2(root);
4990
- if (!isWithin(lexical, rootLexical)) {
4991
- throw new ScriptContainmentError(target, root, raw);
4992
- }
4993
- return lexical;
4994
- }
4995
- function realpathNearest(path) {
4996
- const tail = [];
4997
- let cur = path;
4998
- while (!existsSync5(cur)) {
4999
- const parent = dirname(cur);
5000
- if (parent === cur)
5001
- break;
5002
- tail.unshift(basename(cur));
5003
- cur = parent;
5004
- }
5005
- return join7(realpathSync(cur), ...tail);
5006
- }
5007
- function assertContainedReal(root, target, raw) {
5008
- const rootReal = realpathSync(root);
5009
- const targetReal = realpathNearest(target);
5010
- if (!isWithin(targetReal, rootReal)) {
5011
- throw new ScriptContainmentError(target, root, raw);
5012
- }
5013
- }
5014
- function assertContained(target, root, raw) {
5015
- const lexical = assertContainedLexical(target, root, raw);
5016
- assertContainedReal(root, lexical, raw);
5017
- return lexical;
5018
- }
5019
- function isInlineScript(manifest) {
5020
- return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
5021
- `);
5022
- }
5023
- function scriptRelFor(manifest) {
5024
- if (isInlineScript(manifest)) {
5025
- const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
5026
- return `script${ext}`;
5027
- }
5028
- const rel = normalize(manifest.script);
5029
- if (isAbsolute(rel)) {
5030
- throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
5031
- }
5032
- return rel;
5033
- }
5034
- function resolveScript(manifest, manifestDir) {
5035
- if (isInlineScript(manifest))
5036
- return { path: scriptRelFor(manifest), content: manifest.script };
5037
- const rel = scriptRelFor(manifest);
5038
- const resolved = assertContained(join7(manifestDir, rel), manifestDir, manifest.script);
5039
- if (!existsSync5(resolved)) {
5040
- throw new Error(`manifest script file not found: ${resolved}`);
5041
- }
5042
- return { path: rel, content: readFileSync4(resolved, "utf-8") };
5043
- }
5044
- function customHookDir(name) {
5045
- return join7(getCustomHooksDir(), shortManifestName(name));
5046
- }
5047
- function readCustomManifest(name) {
5048
- const dir = customHookDir(name);
5049
- const manifestPath = join7(dir, "manifest.json");
5050
- if (!existsSync5(manifestPath))
5051
- return;
5052
- try {
5053
- const manifest = parseManifest(readFileSync4(manifestPath, "utf-8"));
5054
- const script = resolveScript(manifest, dir);
5055
- const scriptIsInline = isInlineScript(manifest);
5056
- return { manifest, scriptPath: join7(dir, script.path), scriptContent: script.content, scriptIsInline };
5057
- } catch (err) {
5058
- if (err instanceof ScriptContainmentError)
5059
- throw err;
5060
- return;
5061
- }
5062
- }
5063
- function listCustomHooks() {
5064
- const dir = getCustomHooksDir();
5065
- if (!existsSync5(dir))
5066
- return [];
5067
- const results = [];
5068
- for (const entry of readdirSafe(dir)) {
5069
- const parsed = readCustomManifest(entry);
5070
- if (parsed)
5071
- results.push(parsed);
5072
- }
5073
- return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
4679
+ return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
5074
4680
  }
5075
4681
  function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
5076
4682
  const dir = customHookDir(name);
5077
4683
  if (isAbsolute(scriptRelPath)) {
5078
4684
  throw new ScriptContainmentError(scriptRelPath, dir);
5079
4685
  }
5080
- const scriptPath = scriptRelPath.includes("/") ? join7(dir, scriptRelPath) : join7(dir, scriptRelPath);
4686
+ const scriptPath = scriptRelPath.includes("/") ? join5(dir, scriptRelPath) : join5(dir, scriptRelPath);
5081
4687
  assertContainedLexical(scriptPath, dir, scriptRelPath);
5082
- mkdirSync3(dir, { recursive: true });
4688
+ mkdirSync2(dir, { recursive: true });
5083
4689
  assertContainedReal(dir, scriptPath, scriptRelPath);
5084
4690
  const scriptDir = dirname(scriptPath);
5085
4691
  if (scriptDir !== dir)
5086
- mkdirSync3(scriptDir, { recursive: true });
5087
- writeFileSync2(scriptPath, scriptContent, "utf-8");
5088
- writeFileSync2(join7(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) + `
5089
4695
  `, "utf-8");
5090
4696
  return { dir, scriptPath };
5091
4697
  }
@@ -5143,7 +4749,7 @@ __export(exports_store, {
5143
4749
  LockFileError: () => LockFileError
5144
4750
  });
5145
4751
  import { createHash } from "crypto";
5146
- import { existsSync as existsSync7, 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";
5147
4753
  import { readFile } from "fs/promises";
5148
4754
  import { randomBytes } from "crypto";
5149
4755
  function sha256Of(input) {
@@ -5205,11 +4811,11 @@ function assertLockShape(parsed, path) {
5205
4811
  }
5206
4812
  function readLock() {
5207
4813
  const path = getLockPath();
5208
- if (!existsSync7(path))
4814
+ if (!existsSync6(path))
5209
4815
  return { hooks: {} };
5210
4816
  let raw;
5211
4817
  try {
5212
- raw = readFileSync5(path, "utf-8");
4818
+ raw = readFileSync3(path, "utf-8");
5213
4819
  } catch {
5214
4820
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
5215
4821
  }
@@ -5224,13 +4830,13 @@ function readLock() {
5224
4830
  }
5225
4831
  function writeLock(lock) {
5226
4832
  const path = getLockPath();
5227
- mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
4833
+ mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
5228
4834
  const sorted = {};
5229
4835
  for (const name of Object.keys(lock.hooks).sort()) {
5230
4836
  sorted[name] = lock.hooks[name];
5231
4837
  }
5232
4838
  const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
5233
- writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
4839
+ writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
5234
4840
  `, "utf-8");
5235
4841
  renameSync(tmp, path);
5236
4842
  return path;
@@ -5306,7 +4912,7 @@ async function verifyScriptHash(name, scriptPath) {
5306
4912
  return checkScriptHash(name, await sha256File(scriptPath));
5307
4913
  }
5308
4914
  function retrustHook(name, scriptPath, version, source) {
5309
- const actual = sha256Of(readFileSync5(scriptPath));
4915
+ const actual = sha256Of(readFileSync3(scriptPath));
5310
4916
  const db = getDb();
5311
4917
  upsertHookRecord(db, {
5312
4918
  name,
@@ -5343,31 +4949,29 @@ __export(exports_profiles, {
5343
4949
  deleteProfile: () => deleteProfile,
5344
4950
  createProfile: () => createProfile
5345
4951
  });
5346
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
5347
- import { join as join10 } from "path";
5348
- function resolveProfilesDir() {
5349
- const newDir = join10(getEffectiveDataRoot(), "profiles");
5350
- const oldDir = join10(getHomeDir(), ".hooks", "profiles");
5351
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
5352
- mkdirSync6(newDir, { recursive: true });
4952
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4953
+ import { join as join8 } from "path";
4954
+ function getProfilesDir() {
4955
+ const newDir = join8(getEffectiveDataRoot(), "profiles");
4956
+ const oldDir = join8(getHomeDir(), ".hooks", "profiles");
4957
+ if (!existsSync8(newDir) && existsSync8(oldDir)) {
4958
+ mkdirSync5(newDir, { recursive: true });
5353
4959
  cpSync2(oldDir, newDir, { recursive: true });
5354
4960
  }
5355
4961
  return newDir;
5356
4962
  }
5357
4963
  function ensureProfilesDir() {
5358
- if (!existsSync9(PROFILES_DIR)) {
5359
- mkdirSync6(PROFILES_DIR, { recursive: true });
4964
+ const dir = getProfilesDir();
4965
+ if (!existsSync8(dir)) {
4966
+ mkdirSync5(dir, { recursive: true });
5360
4967
  }
5361
4968
  }
5362
4969
  function profilePath(id) {
5363
- return join10(PROFILES_DIR, `${id}.json`);
4970
+ return join8(getProfilesDir(), `${id}.json`);
5364
4971
  }
5365
4972
  function shortUuid() {
5366
4973
  return crypto.randomUUID().slice(0, 8);
5367
4974
  }
5368
- function getProfilesDir() {
5369
- return PROFILES_DIR;
5370
- }
5371
4975
  function createProfile(input) {
5372
4976
  ensureProfilesDir();
5373
4977
  const id = shortUuid();
@@ -5382,29 +4986,30 @@ function createProfile(input) {
5382
4986
  if (input.name) {
5383
4987
  profile.name = input.name;
5384
4988
  }
5385
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
4989
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5386
4990
  `);
5387
4991
  return profile;
5388
4992
  }
5389
4993
  function getProfile(id) {
5390
4994
  const path = profilePath(id);
5391
4995
  try {
5392
- if (!existsSync9(path))
4996
+ if (!existsSync8(path))
5393
4997
  return null;
5394
- return JSON.parse(readFileSync7(path, "utf-8"));
4998
+ return JSON.parse(readFileSync5(path, "utf-8"));
5395
4999
  } catch {
5396
5000
  return null;
5397
5001
  }
5398
5002
  }
5399
5003
  function listProfiles() {
5400
- if (!existsSync9(PROFILES_DIR))
5004
+ const dir = getProfilesDir();
5005
+ if (!existsSync8(dir))
5401
5006
  return [];
5402
5007
  try {
5403
- const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
5008
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
5404
5009
  const profiles = [];
5405
5010
  for (const file of files) {
5406
5011
  try {
5407
- const content = readFileSync7(join10(PROFILES_DIR, file), "utf-8");
5012
+ const content = readFileSync5(join8(dir, file), "utf-8");
5408
5013
  profiles.push(JSON.parse(content));
5409
5014
  } catch {}
5410
5015
  }
@@ -5421,16 +5026,16 @@ function updateProfile(id, data) {
5421
5026
  profile.name = data.name;
5422
5027
  if (data.preferences !== undefined)
5423
5028
  profile.preferences = data.preferences;
5424
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5029
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5425
5030
  `);
5426
5031
  return profile;
5427
5032
  }
5428
5033
  function deleteProfile(id) {
5429
5034
  const path = profilePath(id);
5430
- if (!existsSync9(path))
5035
+ if (!existsSync8(path))
5431
5036
  return false;
5432
5037
  try {
5433
- rmSync2(path);
5038
+ rmSync(path);
5434
5039
  return true;
5435
5040
  } catch {
5436
5041
  return false;
@@ -5441,7 +5046,7 @@ function touchProfile(id) {
5441
5046
  if (!profile)
5442
5047
  return;
5443
5048
  profile.last_seen_at = new Date().toISOString();
5444
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5049
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5445
5050
  `);
5446
5051
  }
5447
5052
  function exportProfiles() {
@@ -5457,25 +5062,23 @@ function importProfiles(profiles) {
5457
5062
  continue;
5458
5063
  }
5459
5064
  const path = profilePath(profile.agent_id);
5460
- if (existsSync9(path)) {
5065
+ if (existsSync8(path)) {
5461
5066
  skipped++;
5462
5067
  continue;
5463
5068
  }
5464
- writeFileSync5(path, JSON.stringify(profile, null, 2) + `
5069
+ writeFileSync4(path, JSON.stringify(profile, null, 2) + `
5465
5070
  `);
5466
5071
  imported++;
5467
5072
  }
5468
5073
  return { imported, skipped };
5469
5074
  }
5470
- var PROFILES_DIR;
5471
5075
  var init_profiles = __esm(() => {
5472
5076
  init_app_home();
5473
- PROFILES_DIR = resolveProfilesDir();
5474
5077
  });
5475
5078
 
5476
5079
  // src/lib/hook-env.ts
5477
- import { dirname as dirname5, isAbsolute as isAbsolute2 } from "path";
5478
- import { existsSync as existsSync11, statSync as statSync2 } from "fs";
5080
+ import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
5081
+ import { existsSync as existsSync10, statSync } from "fs";
5479
5082
  function isDeniedEnvName(name) {
5480
5083
  const upper = name.toUpperCase();
5481
5084
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5493,7 +5096,7 @@ function isBashFunctionEnvName(name) {
5493
5096
  return name.startsWith("BASH_FUNC_");
5494
5097
  }
5495
5098
  function isUnsafePathEntry(entry, home) {
5496
- if (entry === "" || !isAbsolute2(entry))
5099
+ if (entry === "" || !isAbsolute3(entry))
5497
5100
  return true;
5498
5101
  if (home && (entry === home || entry.startsWith(home + "/")))
5499
5102
  return true;
@@ -5502,14 +5105,14 @@ function isUnsafePathEntry(entry, home) {
5502
5105
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5503
5106
  return true;
5504
5107
  try {
5505
- if ((statSync2(entry).mode & 2) !== 0)
5108
+ if ((statSync(entry).mode & 2) !== 0)
5506
5109
  return true;
5507
5110
  } catch {}
5508
5111
  return false;
5509
5112
  }
5510
5113
  function sanitizeHookPath(pathValue, home) {
5511
5114
  const baseline = PATH_BASELINE.slice();
5512
- if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
5115
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
5513
5116
  baseline.push("/opt/homebrew/bin");
5514
5117
  const bunDir = dirname5(process.execPath);
5515
5118
  const seen = new Set;
@@ -5634,8 +5237,8 @@ __export(exports_run, {
5634
5237
  HookTimeoutError: () => HookTimeoutError
5635
5238
  });
5636
5239
  import { randomBytes as randomBytes2 } from "crypto";
5637
- import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
5638
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join13 } 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";
5639
5242
  function extensionOf(scriptPath) {
5640
5243
  const base = basename2(scriptPath);
5641
5244
  const dot = base.lastIndexOf(".");
@@ -5706,7 +5309,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5706
5309
  async function executeVerifiedScript(options) {
5707
5310
  const scriptDir = dirname6(options.scriptPath);
5708
5311
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5709
- const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5312
+ const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5710
5313
  let proc = null;
5711
5314
  let timedOut = false;
5712
5315
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -5714,8 +5317,8 @@ async function executeVerifiedScript(options) {
5714
5317
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
5715
5318
  }, options.timeout) : null;
5716
5319
  try {
5717
- writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5718
- 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 ?? []], {
5719
5322
  stdin: new Response(options.stdin),
5720
5323
  stdout: "pipe",
5721
5324
  stderr: "pipe",
@@ -5736,7 +5339,7 @@ async function executeVerifiedScript(options) {
5736
5339
  if (timeoutTimer)
5737
5340
  clearTimeout(timeoutTimer);
5738
5341
  try {
5739
- rmSync4(tempPath, { force: true });
5342
+ rmSync3(tempPath, { force: true });
5740
5343
  } catch {}
5741
5344
  }
5742
5345
  }
@@ -6395,6 +5998,18 @@ var HOOKS = [
6395
5998
  matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch)$",
6396
5999
  tags: ["workspace", "repos", "structure", "guard", "safety", "orgs", "multi-agent"]
6397
6000
  },
6001
+ {
6002
+ name: "trash-guard",
6003
+ displayName: "Trash Guard",
6004
+ description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
6005
+ version: "0.1.0",
6006
+ category: "Git Safety",
6007
+ event: "PreToolUse",
6008
+ matcher: "Bash",
6009
+ tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
6010
+ rewritesInput: true,
6011
+ timeoutSeconds: 5
6012
+ },
6398
6013
  {
6399
6014
  name: "checktests",
6400
6015
  displayName: "Check Tests",
@@ -6871,21 +6486,21 @@ function getHook(name) {
6871
6486
  return HOOKS.find((h) => h.name === name);
6872
6487
  }
6873
6488
  // src/lib/installer.ts
6874
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
6875
- import { join as join9, dirname as dirname3 } from "path";
6876
- import { homedir as homedir4 } from "os";
6489
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
6490
+ import { join as join7, dirname as dirname3 } from "path";
6491
+ import { homedir as homedir3 } from "os";
6877
6492
  import { fileURLToPath as fileURLToPath2 } from "url";
6878
6493
 
6879
6494
  // src/lib/resolve.ts
6880
- import { dirname as dirname2, join as join8 } from "path";
6881
- import { existsSync as existsSync6 } from "fs";
6495
+ import { dirname as dirname2, join as join6 } from "path";
6496
+ import { existsSync as existsSync5 } from "fs";
6882
6497
  import { fileURLToPath } from "url";
6883
6498
  init_manifest();
6884
6499
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
6885
- const sourceLayout = join8(moduleDir, "..", "..", "hooks");
6886
- if (existsSync6(join8(sourceLayout, "hook-gitguard")))
6500
+ const sourceLayout = join6(moduleDir, "..", "..", "hooks");
6501
+ if (existsSync5(join6(sourceLayout, "hook-gitguard")))
6887
6502
  return sourceLayout;
6888
- return join8(moduleDir, "..", "hooks");
6503
+ return join6(moduleDir, "..", "hooks");
6889
6504
  }
6890
6505
  function resolveHookDir(name) {
6891
6506
  const custom2 = readCustomManifest(name);
@@ -6896,11 +6511,11 @@ function resolveHookDir(name) {
6896
6511
  return;
6897
6512
  const short = shortManifestName(name);
6898
6513
  const bundledBase = resolveBundledHooksDir();
6899
- const direct = join8(bundledBase, short);
6900
- if (existsSync6(direct))
6514
+ const direct = join6(bundledBase, short);
6515
+ if (existsSync5(direct))
6901
6516
  return direct;
6902
- const prefixed = join8(bundledBase, `hook-${short}`);
6903
- return existsSync6(prefixed) ? prefixed : undefined;
6517
+ const prefixed = join6(bundledBase, `hook-${short}`);
6518
+ return existsSync5(prefixed) ? prefixed : undefined;
6904
6519
  }
6905
6520
  function resolveScriptPath(name) {
6906
6521
  const custom2 = readCustomManifest(name);
@@ -6909,8 +6524,8 @@ function resolveScriptPath(name) {
6909
6524
  const dir = resolveHookDir(name);
6910
6525
  if (!dir)
6911
6526
  return;
6912
- const script = join8(dir, "src", "hook.ts");
6913
- return existsSync6(script) ? script : undefined;
6527
+ const script = join6(dir, "src", "hook.ts");
6528
+ return existsSync5(script) ? script : undefined;
6914
6529
  }
6915
6530
  function resolveHookMeta(name) {
6916
6531
  const custom2 = readCustomManifest(name);
@@ -6963,10 +6578,17 @@ function resolveHook(name) {
6963
6578
 
6964
6579
  // src/lib/installer.ts
6965
6580
  init_manifest();
6966
- init_store();
6967
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
6968
- var HOOKS_DIR = existsSync8(join9(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join9(__dirname2, "..", "..", "hooks") : join9(__dirname2, "..", "hooks");
6969
- function normalizeHookName(name) {
6581
+
6582
+ // src/lib/registration.ts
6583
+ function matchersOverlap(a, b) {
6584
+ return a === b || a.includes(b) || b.includes(a);
6585
+ }
6586
+
6587
+ // src/lib/installer.ts
6588
+ init_store();
6589
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
6590
+ var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
6591
+ function normalizeHookName(name) {
6970
6592
  return name.startsWith("hook-") ? name : `hook-${name}`;
6971
6593
  }
6972
6594
  function shortHookName(name) {
@@ -7035,25 +6657,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
7035
6657
  return globalOverride;
7036
6658
  const dir = getTargetSettingsDir(target);
7037
6659
  if (scope === "project") {
7038
- return target === "codewith" ? join9(process.cwd(), dir, "config.toml") : join9(process.cwd(), dir, "settings.json");
6660
+ return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
7039
6661
  }
7040
- return target === "codewith" ? join9(homedir4(), dir, "config.toml") : join9(homedir4(), dir, "settings.json");
6662
+ return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
7041
6663
  }
7042
6664
  function getHookPath(name) {
7043
6665
  const shortName = shortHookName(name);
7044
6666
  const resolved = resolveHookDir(shortName);
7045
6667
  if (resolved)
7046
6668
  return resolved;
7047
- return join9(HOOKS_DIR, normalizeHookName(shortName));
6669
+ return join7(HOOKS_DIR, normalizeHookName(shortName));
7048
6670
  }
7049
6671
  function hookExists(name) {
7050
- return existsSync8(getHookPath(name));
6672
+ return existsSync7(getHookPath(name));
7051
6673
  }
7052
6674
  function readSettings(scope = "global", target = "claude") {
7053
6675
  const path = getSettingsPath(scope, target);
7054
6676
  try {
7055
- if (existsSync8(path)) {
7056
- return JSON.parse(readFileSync6(path, "utf-8"));
6677
+ if (existsSync7(path)) {
6678
+ return JSON.parse(readFileSync4(path, "utf-8"));
7057
6679
  }
7058
6680
  } catch (error) {
7059
6681
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -7063,10 +6685,10 @@ function readSettings(scope = "global", target = "claude") {
7063
6685
  function writeSettings(settings, scope = "global", target = "claude") {
7064
6686
  const path = getSettingsPath(scope, target);
7065
6687
  const dir = dirname3(path);
7066
- if (!existsSync8(dir)) {
7067
- mkdirSync5(dir, { recursive: true });
6688
+ if (!existsSync7(dir)) {
6689
+ mkdirSync4(dir, { recursive: true });
7068
6690
  }
7069
- writeFileSync4(path, JSON.stringify(settings, null, 2) + `
6691
+ writeFileSync3(path, JSON.stringify(settings, null, 2) + `
7070
6692
  `);
7071
6693
  }
7072
6694
  function getTargetEventName(internalEvent, target) {
@@ -7098,6 +6720,7 @@ function codewithTimeout(name) {
7098
6720
  case "prompt-guard":
7099
6721
  return 3;
7100
6722
  case "worktree-guard":
6723
+ case "trash-guard":
7101
6724
  case "stop-sync":
7102
6725
  return 5;
7103
6726
  default:
@@ -7116,6 +6739,8 @@ function codewithStatusMessage(name) {
7116
6739
  return "Checking prompt safety";
7117
6740
  case "worktree-guard":
7118
6741
  return "Checking worktree safety";
6742
+ case "trash-guard":
6743
+ return "Redirecting rm into trash";
7119
6744
  case "stop-sync":
7120
6745
  return "Syncing turn-end heartbeat";
7121
6746
  default:
@@ -7152,7 +6777,7 @@ function buildCodewithTomlFragment(name, profile) {
7152
6777
  function readCodewithConfig(scope, configPath) {
7153
6778
  const path = getSettingsPath(scope, "codewith", configPath);
7154
6779
  try {
7155
- return existsSync8(path) ? readFileSync6(path, "utf-8") : "";
6780
+ return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7156
6781
  } catch {
7157
6782
  return "";
7158
6783
  }
@@ -7160,13 +6785,13 @@ function readCodewithConfig(scope, configPath) {
7160
6785
  function appendCodewithFragment(fragment, scope, configPath) {
7161
6786
  const path = getSettingsPath(scope, "codewith", configPath);
7162
6787
  const dir = dirname3(path);
7163
- if (!existsSync8(dir))
7164
- mkdirSync5(dir, { recursive: true });
7165
- const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
6788
+ if (!existsSync7(dir))
6789
+ mkdirSync4(dir, { recursive: true });
6790
+ const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7166
6791
  const sep2 = existing.trim() ? `
7167
6792
 
7168
6793
  ` : "";
7169
- writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6794
+ writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
7170
6795
  return path;
7171
6796
  }
7172
6797
  function detectConflict(name, scope, target) {
@@ -7183,14 +6808,32 @@ function detectConflict(name, scope, target) {
7183
6808
  continue;
7184
6809
  if (!getHookEvents(existing).some((event) => events.has(event)))
7185
6810
  continue;
7186
- const a = meta.matcher.toLowerCase();
7187
- const b = existing.matcher.toLowerCase();
7188
- if (a === b || a.includes(b) || b.includes(a)) {
6811
+ if (matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
7189
6812
  return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
7190
6813
  }
7191
6814
  }
7192
6815
  return;
7193
6816
  }
6817
+ function detectRewriteConflict(name, scope, target, lookup = getHook) {
6818
+ const meta = resolveHookMeta(name);
6819
+ if (!meta?.matcher || !meta.rewritesInput)
6820
+ return;
6821
+ if (!getHookEvents(meta).includes("PreToolUse"))
6822
+ return;
6823
+ for (const existingName of getRegisteredHooksForTarget(scope, target)) {
6824
+ if (existingName === name)
6825
+ continue;
6826
+ const existing = lookup(existingName);
6827
+ if (!existing?.matcher || !existing.rewritesInput)
6828
+ continue;
6829
+ if (!getHookEvents(existing).includes("PreToolUse"))
6830
+ continue;
6831
+ if (!matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase()))
6832
+ continue;
6833
+ return `both '${name}' and '${existingName}' rewrite the tool input on overlapping PreToolUse matchers ('${meta.matcher}' / '${existing.matcher}'). Only one rewrite per tool call is applied, so one guard would silently disappear \u2014 install one of them, or narrow a matcher so they no longer overlap.`;
6834
+ }
6835
+ return;
6836
+ }
7194
6837
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
7195
6838
  const shortName = shortHookName(name);
7196
6839
  if (!hookExists(shortName)) {
@@ -7250,6 +6893,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
7250
6893
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
7251
6894
  }
7252
6895
  const conflict = detectConflict(shortName, scope, target);
6896
+ const rewriteConflict = detectRewriteConflict(shortName, scope, target);
6897
+ if (rewriteConflict) {
6898
+ return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
6899
+ }
7253
6900
  try {
7254
6901
  registerHook(shortName, scope, target, profile);
7255
6902
  return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
@@ -7321,8 +6968,12 @@ function registerHook(name, scope = "global", target = "claude", profile) {
7321
6968
  for (const eventKey of uniqueEventKeys) {
7322
6969
  if (!settings.hooks[eventKey])
7323
6970
  settings.hooks[eventKey] = [];
6971
+ const hookEntry = { type: "command", command: hookCommand };
6972
+ if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0) {
6973
+ hookEntry.timeout = meta.timeoutSeconds;
6974
+ }
7324
6975
  const entry = {
7325
- hooks: [{ type: "command", command: hookCommand }]
6976
+ hooks: [hookEntry]
7326
6977
  };
7327
6978
  if (meta.matcher) {
7328
6979
  entry.matcher = meta.matcher;
@@ -7463,14 +7114,28 @@ function codewithHasHookEntryInText(configText, name) {
7463
7114
  }
7464
7115
  function codewithHasHookEntry(name, scope = "global") {
7465
7116
  const path = getSettingsPath(scope, "codewith");
7466
- if (!existsSync8(path))
7117
+ if (!existsSync7(path))
7467
7118
  return false;
7468
7119
  try {
7469
- return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
7120
+ return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
7470
7121
  } catch {
7471
7122
  return false;
7472
7123
  }
7473
7124
  }
7125
+ function removeStoreDirFailClosed(dir) {
7126
+ const entries = readdirSync3(dir, { withFileTypes: true });
7127
+ for (const entry of entries) {
7128
+ if (entry.isDirectory()) {
7129
+ removeStoreDirFailClosed(join7(dir, entry.name));
7130
+ }
7131
+ }
7132
+ for (const entry of entries) {
7133
+ if (!entry.isDirectory()) {
7134
+ unlinkSync(join7(dir, entry.name));
7135
+ }
7136
+ }
7137
+ rmdirSync(dir);
7138
+ }
7474
7139
  function uninstallHook(name, scope = "global", target = "claude") {
7475
7140
  const shortName = shortHookName(name);
7476
7141
  const custom2 = readCustomManifest(shortName);
@@ -7497,11 +7162,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
7497
7162
  const registrationsRemaining = [];
7498
7163
  if (target === "codewith" || target === "all") {
7499
7164
  const codewithPath = getSettingsPath(scope, "codewith");
7500
- if (existsSync8(codewithPath)) {
7501
- const before = readFileSync6(codewithPath, "utf-8");
7165
+ if (existsSync7(codewithPath)) {
7166
+ const before = readFileSync4(codewithPath, "utf-8");
7502
7167
  const after = removeCodewithHookEntry(before, shortName);
7503
7168
  if (after.removed) {
7504
- writeFileSync4(codewithPath, after.text, "utf-8");
7169
+ writeFileSync3(codewithPath, after.text, "utf-8");
7505
7170
  }
7506
7171
  if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
7507
7172
  registrationsRemaining.push("codewith");
@@ -7510,7 +7175,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
7510
7175
  }
7511
7176
  if (custom2) {
7512
7177
  try {
7513
- rmSync(customHookDir(shortName), { recursive: true, force: true });
7178
+ removeStoreDirFailClosed(customHookDir(shortName));
7514
7179
  } catch {
7515
7180
  return {
7516
7181
  name: shortName,
@@ -7540,22 +7205,891 @@ function uninstallHook(name, scope = "global", target = "claude") {
7540
7205
  // src/index.ts
7541
7206
  init_profiles();
7542
7207
  init_config();
7208
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
7209
+
7210
+ // ../contracts/dist/client/transport.js
7211
+ import { isIP } from "net";
7212
+ import { spawnSync } from "child_process";
7213
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
7214
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
7215
+ import { createRequire } from "module";
7216
+ import { hostname as osHostname } from "os";
7217
+ import { isAbsolute as isAbsolute2, join as join9 } from "path";
7218
+ function envToken(name) {
7219
+ return name.toUpperCase().replace(/-/g, "_");
7220
+ }
7221
+ function clientTransportEnvKeys(name) {
7222
+ const envSegment = envToken(name);
7223
+ return {
7224
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
7225
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
7226
+ };
7227
+ }
7228
+ function credentialOverrideEnvKey(name) {
7229
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
7230
+ }
7231
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
7232
+ function credentialPointerEnvKey(name) {
7233
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
7234
+ }
7235
+
7236
+ class CredentialResolutionError extends Error {
7237
+ appName;
7238
+ attempted;
7239
+ constructor(appName, message, attempted) {
7240
+ super(message);
7241
+ this.name = "CredentialResolutionError";
7242
+ this.appName = appName;
7243
+ this.attempted = attempted;
7244
+ }
7245
+ }
7246
+
7247
+ class CredentialFileUnsafeError extends Error {
7248
+ path;
7249
+ constructor(path, reason) {
7250
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
7251
+ this.name = "CredentialFileUnsafeError";
7252
+ this.path = path;
7253
+ }
7254
+ }
7255
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
7256
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
7257
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
7258
+ var HASNA_HOME_DIR = ".hasna";
7259
+ var CONFIG_SUBDIR = "config";
7260
+ var CREDENTIALS_FILE = "credentials";
7261
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
7262
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
7263
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
7264
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
7265
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
7266
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7267
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
7268
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
7269
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
7270
+ function homeDir(env) {
7271
+ const home = env.HOME?.trim();
7272
+ return home ? home : null;
7273
+ }
7274
+ function absoluteOverride(env, key) {
7275
+ const value = env[key]?.trim();
7276
+ return value && isAbsolute2(value) ? value : null;
7277
+ }
7278
+ function hasnaHomeDir(env) {
7279
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
7280
+ if (override)
7281
+ return override;
7282
+ const home = homeDir(env);
7283
+ return home ? join9(home, HASNA_HOME_DIR) : null;
7284
+ }
7285
+ function appConfigDir(name, env) {
7286
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
7287
+ if (configRoot)
7288
+ return join9(configRoot, name);
7289
+ const root = hasnaHomeDir(env);
7290
+ return root ? join9(root, name, CONFIG_SUBDIR) : null;
7291
+ }
7292
+ function credentialDiskSourceList(name, env, profile = null) {
7293
+ if (!SAFE_APP_SLUG.test(name))
7294
+ return [];
7295
+ const directory = appConfigDir(name, env);
7296
+ if (!directory)
7297
+ return [];
7298
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
7299
+ return [{ path: join9(directory, file), tier: "disk" }];
7300
+ }
7301
+ function credentialDiskSources(name, env) {
7302
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
7303
+ }
7304
+ function profileDiskSources(name, env, profile) {
7305
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
7306
+ }
7307
+ function parseEnvFile(text) {
7308
+ const values = new Map;
7309
+ const unusable = new Set;
7310
+ for (const rawLine of text.split(/\r?\n/)) {
7311
+ const line = rawLine.trim();
7312
+ if (line.length === 0 || line.startsWith("#"))
7313
+ continue;
7314
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
7315
+ const equals = withoutExport.indexOf("=");
7316
+ if (equals <= 0)
7317
+ continue;
7318
+ const key = withoutExport.slice(0, equals).trim();
7319
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
7320
+ continue;
7321
+ let value = withoutExport.slice(equals + 1).trim();
7322
+ const quote = value[0];
7323
+ if (quote === '"' || quote === "'") {
7324
+ if (value.length < 2 || !value.endsWith(quote)) {
7325
+ unusable.add(key);
7326
+ continue;
7327
+ }
7328
+ value = value.slice(1, -1);
7329
+ }
7330
+ if (value.trim().length === 0) {
7331
+ unusable.add(key);
7332
+ continue;
7333
+ }
7334
+ if (values.has(key) && values.get(key) !== value)
7335
+ unusable.add(key);
7336
+ values.set(key, value);
7337
+ }
7338
+ return { values, unusable };
7339
+ }
7340
+ function configFileModeAllowed(mode) {
7341
+ const permissions = mode & 4095;
7342
+ return permissions === 256 || permissions === 384;
7343
+ }
7344
+ function configFileReadsCoherent(before, after) {
7345
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
7346
+ }
7347
+ function readAppConfigFile(path) {
7348
+ const unsafe = (reason) => {
7349
+ throw new CredentialFileUnsafeError(path, reason);
7350
+ };
7351
+ let fd = -1;
7352
+ try {
7353
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
7354
+ } catch (error) {
7355
+ const code = error.code;
7356
+ if (code === "ENOENT" || code === "ENOTDIR")
7357
+ return null;
7358
+ if (code === "ELOOP")
7359
+ unsafe("the path is a symlink");
7360
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
7361
+ }
7362
+ try {
7363
+ const before = fstatSync(fd);
7364
+ if (!before.isFile())
7365
+ unsafe("the path is not a regular file");
7366
+ if (!configFileModeAllowed(before.mode)) {
7367
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
7368
+ }
7369
+ const uid = process.getuid?.() ?? process.geteuid?.();
7370
+ if (uid !== undefined && before.uid !== uid)
7371
+ unsafe("the file is not owned by the current user");
7372
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
7373
+ unsafe("the file exceeds the size limit");
7374
+ const bytes = readFileSync6(fd);
7375
+ const after = fstatSync(fd);
7376
+ if (!configFileReadsCoherent(before, after)) {
7377
+ unsafe("the file changed while being read");
7378
+ }
7379
+ return parseEnvFile(bytes.toString("utf8"));
7380
+ } finally {
7381
+ if (fd !== -1)
7382
+ closeSync(fd);
7383
+ }
7384
+ }
7385
+ function readCredentialFile(path, apiKeyKeys) {
7386
+ const parsed = readAppConfigFile(path);
7387
+ if (!parsed)
7388
+ return null;
7389
+ for (const key of apiKeyKeys) {
7390
+ if (parsed.unusable.has(key)) {
7391
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
7392
+ }
7393
+ }
7394
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7395
+ if (new Set(values).size > 1) {
7396
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
7397
+ }
7398
+ return values[0] ?? null;
7399
+ }
7400
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
7401
+ function appConfigDiskValue(name, env, keys) {
7402
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
7403
+ if (wanted.length === 0)
7404
+ return null;
7405
+ for (const path of credentialDiskSources(name, env)) {
7406
+ const parsed = readAppConfigFile(path);
7407
+ if (!parsed)
7408
+ continue;
7409
+ if (wanted.some((key) => parsed.unusable.has(key))) {
7410
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
7411
+ }
7412
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7413
+ if (new Set(values).size > 1)
7414
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
7415
+ for (const key of wanted) {
7416
+ if (parsed.unusable.has(key))
7417
+ return { key, value: "", path, unusable: true };
7418
+ const value = parsed.values.get(key)?.trim();
7419
+ if (value)
7420
+ return { key, value, path };
7421
+ }
7422
+ }
7423
+ return null;
7424
+ }
7425
+ function assertUsableCredential(appName, source, value) {
7426
+ if (VAULT_POINTER_SHAPE.test(value)) {
7427
+ 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]);
7428
+ }
7429
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
7430
+ return;
7431
+ 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]);
7432
+ }
7433
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
7434
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
7435
+ function sealCredential(fields) {
7436
+ const { apiKey } = fields;
7437
+ const visible = {
7438
+ tier: fields.tier,
7439
+ source: fields.source,
7440
+ deliberate: fields.deliberate,
7441
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
7442
+ warning: fields.warning
7443
+ };
7444
+ const sealed = { ...visible };
7445
+ Object.defineProperty(sealed, "apiKey", {
7446
+ value: apiKey,
7447
+ enumerable: false,
7448
+ writable: false,
7449
+ configurable: false
7450
+ });
7451
+ if (fields.pointerVaultKey !== undefined) {
7452
+ Object.defineProperty(sealed, "pointerVaultKey", {
7453
+ value: fields.pointerVaultKey,
7454
+ enumerable: false,
7455
+ writable: false,
7456
+ configurable: false
7457
+ });
7458
+ }
7459
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
7460
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
7461
+ enumerable: false,
7462
+ writable: false,
7463
+ configurable: false
7464
+ });
7465
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
7466
+ value: true,
7467
+ enumerable: false,
7468
+ writable: false,
7469
+ configurable: false
7470
+ });
7471
+ return Object.freeze(sealed);
7472
+ }
7473
+ function firstEnvValue(env, keys) {
7474
+ for (const key of keys) {
7475
+ if (!Object.prototype.hasOwnProperty.call(env, key))
7476
+ continue;
7477
+ const value = env[key]?.trim();
7478
+ if (value)
7479
+ return { key, value };
7480
+ }
7481
+ return null;
7482
+ }
7483
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7484
+ function isAmbientEnvironment(env) {
7485
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
7486
+ }
7487
+ function defaultKeychainRunner(argv) {
7488
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
7489
+ encoding: "utf8",
7490
+ stdio: ["ignore", "pipe", "pipe"],
7491
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
7492
+ });
7493
+ return {
7494
+ status: result.status,
7495
+ stdout: result.stdout ?? "",
7496
+ stderr: result.error ? result.error.message : result.stderr ?? ""
7497
+ };
7498
+ }
7499
+ function keychainTierEnabled(env, options) {
7500
+ if ((options.platform ?? process.platform) !== "darwin")
7501
+ return false;
7502
+ if (options.enabled !== undefined)
7503
+ return options.enabled;
7504
+ return options.run !== undefined || isAmbientEnvironment(env);
7505
+ }
7506
+ function keychainAccount(env, options) {
7507
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
7508
+ if (station)
7509
+ return station;
7510
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
7511
+ if (host)
7512
+ return host;
7513
+ const user = env.USER?.trim();
7514
+ return user || null;
7515
+ }
7516
+ function keychainFailureHint(text) {
7517
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
7518
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
7519
+ return clean ? `: ${clean}` : "";
7520
+ }
7521
+ function readKeychainItem(name, env, kind, options) {
7522
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
7523
+ return null;
7524
+ const account = keychainAccount(env, options);
7525
+ if (!account)
7526
+ return null;
7527
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
7528
+ const source = `keychain:${service}@${account}`;
7529
+ const run = options.run ?? defaultKeychainRunner;
7530
+ let result;
7531
+ try {
7532
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
7533
+ } catch (error) {
7534
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
7535
+ 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]);
7536
+ }
7537
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
7538
+ return null;
7539
+ if (result.status !== 0) {
7540
+ 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]);
7541
+ }
7542
+ const value = result.stdout.trim();
7543
+ if (!value) {
7544
+ 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]);
7545
+ }
7546
+ return { value, source };
7547
+ }
7548
+ function keychainConfigValue(name, env, options = {}) {
7549
+ return readKeychainItem(name, env, "api-url", options);
7550
+ }
7551
+ function snapshotClientEnvironment(name, env) {
7552
+ const keys = clientTransportEnvKeys(name);
7553
+ const ambient = isAmbientEnvironment(env);
7554
+ const snapshot = Object.create(null);
7555
+ for (const key of [
7556
+ ...keys.apiUrlKeys,
7557
+ ...keys.apiKeyKeys,
7558
+ credentialOverrideEnvKey(name),
7559
+ credentialPointerEnvKey(name),
7560
+ CREDENTIAL_PROFILE_ENV_KEY,
7561
+ "HOME",
7562
+ HASNA_HOME_ENV_KEY,
7563
+ HASNA_CONFIG_HOME_ENV_KEY,
7564
+ KEYCHAIN_STATION_ENV_KEY,
7565
+ "USER"
7566
+ ]) {
7567
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
7568
+ if (!descriptor)
7569
+ continue;
7570
+ if (!("value" in descriptor)) {
7571
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
7572
+ }
7573
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
7574
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
7575
+ }
7576
+ snapshot[key] = descriptor.value;
7577
+ }
7578
+ if (ambient) {
7579
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
7580
+ value: true,
7581
+ enumerable: false,
7582
+ writable: false,
7583
+ configurable: false
7584
+ });
7585
+ }
7586
+ return Object.freeze(snapshot);
7587
+ }
7588
+ function resolveCredential(name, env, options = {}) {
7589
+ env = snapshotClientEnvironment(name, env);
7590
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
7591
+ const diskPaths = credentialDiskSources(name, env);
7592
+ if (options.apiKey !== undefined) {
7593
+ const explicitKey = options.apiKey.trim();
7594
+ if (!explicitKey) {
7595
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
7596
+ }
7597
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
7598
+ return sealCredential({
7599
+ apiKey: explicitKey,
7600
+ tier: "argument",
7601
+ source: "explicit apiKey argument",
7602
+ deliberate: true,
7603
+ diskCandidates: diskPaths,
7604
+ warning: null
7605
+ });
7606
+ }
7607
+ const overrideKeyName = credentialOverrideEnvKey(name);
7608
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
7609
+ if (overrideRaw !== undefined) {
7610
+ const override = overrideRaw.trim();
7611
+ if (!override) {
7612
+ 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]);
7613
+ }
7614
+ assertUsableCredential(name, overrideKeyName, override);
7615
+ return sealCredential({
7616
+ apiKey: override,
7617
+ tier: "override",
7618
+ source: overrideKeyName,
7619
+ deliberate: true,
7620
+ diskCandidates: diskPaths,
7621
+ warning: null
7622
+ });
7623
+ }
7624
+ const pointerKeyName = credentialPointerEnvKey(name);
7625
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
7626
+ if (pointerRaw !== undefined) {
7627
+ const pointer = pointerRaw.trim();
7628
+ if (!pointer) {
7629
+ 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]);
7630
+ }
7631
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
7632
+ 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]);
7633
+ }
7634
+ return sealCredential({
7635
+ apiKey: "",
7636
+ pointerVaultKey: pointer,
7637
+ tier: "pointer",
7638
+ source: pointerKeyName,
7639
+ deliberate: true,
7640
+ diskCandidates: diskPaths,
7641
+ warning: null
7642
+ });
7643
+ }
7644
+ if (options.profile !== undefined && !options.profile.trim()) {
7645
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
7646
+ }
7647
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
7648
+ if (profileRaw !== undefined && !profileRaw.trim()) {
7649
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
7650
+ }
7651
+ const profile = options.profile?.trim() || profileRaw?.trim();
7652
+ if (profile) {
7653
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
7654
+ if (!SAFE_PROFILE.test(profile)) {
7655
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
7656
+ }
7657
+ const paths = profileDiskSources(name, env, profile);
7658
+ for (const path of paths) {
7659
+ const value = readCredentialFile(path, apiKeyKeys);
7660
+ if (value) {
7661
+ assertUsableCredential(name, path, value);
7662
+ return sealCredential({
7663
+ apiKey: value,
7664
+ tier: "profile",
7665
+ source: path,
7666
+ deliberate: true,
7667
+ diskCandidates: paths,
7668
+ warning: null
7669
+ });
7670
+ }
7671
+ }
7672
+ 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);
7673
+ }
7674
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
7675
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
7676
+ if (blankEnv) {
7677
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
7678
+ }
7679
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
7680
+ 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));
7681
+ }
7682
+ const envHit = firstEnvValue(env, apiKeyKeys);
7683
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
7684
+ if (keychainHit) {
7685
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
7686
+ 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;
7687
+ return sealCredential({
7688
+ apiKey: keychainHit.value,
7689
+ tier: "keychain",
7690
+ source: keychainHit.source,
7691
+ deliberate: false,
7692
+ diskCandidates: diskPaths,
7693
+ warning
7694
+ });
7695
+ }
7696
+ const diskSourceList = credentialDiskSourceList(name, env, null);
7697
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
7698
+ if (diskHits.length > 0) {
7699
+ const winner = diskHits[0];
7700
+ assertUsableCredential(name, winner.src.path, winner.value);
7701
+ const divergentSources = [
7702
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
7703
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
7704
+ ];
7705
+ 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;
7706
+ return sealCredential({
7707
+ apiKey: winner.value,
7708
+ tier: winner.src.tier,
7709
+ source: winner.src.path,
7710
+ deliberate: false,
7711
+ diskCandidates: diskPaths,
7712
+ warning
7713
+ });
7714
+ }
7715
+ if (envHit) {
7716
+ assertUsableCredential(name, envHit.key, envHit.value);
7717
+ return sealCredential({
7718
+ apiKey: envHit.value,
7719
+ tier: "env",
7720
+ source: envHit.key,
7721
+ deliberate: false,
7722
+ diskCandidates: diskPaths,
7723
+ warning: null
7724
+ });
7725
+ }
7726
+ return null;
7727
+ }
7728
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
7729
+ var requireSecretsSdk = createRequire(import.meta.url);
7730
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
7731
+ var DEFAULT_AUTHORITY_SOURCE = "default";
7732
+ function defaultFleetGatewayBaseUrl(name) {
7733
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
7734
+ }
7735
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
7736
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7737
+ function isValidDnsDomain(value) {
7738
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
7739
+ return false;
7740
+ }
7741
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
7742
+ }
7743
+ function validateAppSlug(name) {
7744
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
7745
+ throw new Error("App name must be one lowercase DNS label.");
7746
+ }
7747
+ return name;
7748
+ }
7749
+ function rawAuthority(value) {
7750
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
7751
+ if (!match)
7752
+ throw new Error("API URL must be absolute.");
7753
+ const afterScheme = value.slice(match[0].length);
7754
+ const boundary = afterScheme.search(/[/?#]/);
7755
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
7756
+ if (!authority)
7757
+ throw new Error("API URL must include a hostname.");
7758
+ return authority;
7759
+ }
7760
+ function assertCanonicalPort(port) {
7761
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
7762
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7763
+ }
7764
+ const numericPort = Number(port);
7765
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
7766
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7767
+ }
7768
+ }
7769
+ function canonicalAuthorityHostname(authority) {
7770
+ let rawHostname;
7771
+ if (authority.startsWith("[")) {
7772
+ const closingBracket = authority.indexOf("]");
7773
+ if (closingBracket === -1) {
7774
+ throw new Error("API URL authority must contain a canonical hostname.");
7775
+ }
7776
+ rawHostname = authority.slice(0, closingBracket + 1);
7777
+ const portSuffix = authority.slice(closingBracket + 1);
7778
+ if (portSuffix) {
7779
+ if (!portSuffix.startsWith(":")) {
7780
+ throw new Error("API URL authority must contain a canonical hostname and port.");
7781
+ }
7782
+ assertCanonicalPort(portSuffix.slice(1));
7783
+ }
7784
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
7785
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
7786
+ }
7787
+ } else {
7788
+ const firstColon = authority.indexOf(":");
7789
+ const lastColon = authority.lastIndexOf(":");
7790
+ if (firstColon !== lastColon) {
7791
+ throw new Error("IPv6 API URL authorities must use brackets.");
7792
+ }
7793
+ if (lastColon !== -1) {
7794
+ const port = authority.slice(lastColon + 1);
7795
+ assertCanonicalPort(port);
7796
+ rawHostname = authority.slice(0, lastColon);
7797
+ } else {
7798
+ rawHostname = authority;
7799
+ }
7800
+ const ipVersion = isIP(rawHostname);
7801
+ const numericAddressParts = rawHostname.split(".");
7802
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
7803
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
7804
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
7805
+ }
7806
+ }
7807
+ return rawHostname.toLowerCase();
7808
+ }
7809
+ function isDeliberateLoopbackHttpAuthority(authority) {
7810
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
7811
+ }
7812
+ function toV1BaseUrl(apiUrl) {
7813
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
7814
+ throw new Error("API URL must not contain ASCII control characters.");
7815
+ }
7816
+ const input = apiUrl.trim();
7817
+ const authority = rawAuthority(input);
7818
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
7819
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
7820
+ }
7821
+ const canonicalHostname = canonicalAuthorityHostname(authority);
7822
+ const url = new URL(input);
7823
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7824
+ throw new Error("API URL must use http or https.");
7825
+ }
7826
+ if (url.username || url.password) {
7827
+ throw new Error("API URL must not include credentials.");
7828
+ }
7829
+ if (!url.hostname || url.hostname.endsWith(".")) {
7830
+ throw new Error("API URL must include a canonical hostname.");
7831
+ }
7832
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
7833
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
7834
+ }
7835
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
7836
+ throw new Error("API URL must not use IDN or punycode hostnames.");
7837
+ }
7838
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
7839
+ throw new Error("API URL may use http only for an exact loopback authority.");
7840
+ }
7841
+ if (url.search || url.hash) {
7842
+ throw new Error("API URL must not include a query string or fragment.");
7843
+ }
7844
+ let path = url.pathname.replace(/\/+$/, "");
7845
+ if (path.endsWith("/v1"))
7846
+ path = path.slice(0, -"/v1".length);
7847
+ url.pathname = `${path}/v1`;
7848
+ return url.toString().replace(/\/+$/, "");
7849
+ }
7850
+ class ClientTransportConfigurationError extends Error {
7851
+ appName;
7852
+ sources;
7853
+ constructor(appName, message, sources = []) {
7854
+ super(message);
7855
+ this.name = "ClientTransportConfigurationError";
7856
+ this.appName = appName;
7857
+ this.sources = Object.freeze([...sources]);
7858
+ }
7859
+ }
7860
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
7861
+ env = snapshotClientEnvironment(name, env);
7862
+ const keys = clientTransportEnvKeys(name);
7863
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
7864
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
7865
+ if (blankUrl) {
7866
+ 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]);
7867
+ }
7868
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
7869
+ if (controlledUrl) {
7870
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
7871
+ }
7872
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
7873
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
7874
+ 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));
7875
+ }
7876
+ const envUrlHit = usableUrlEntries[0] ?? null;
7877
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
7878
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
7879
+ if (diskConfigUrlHit?.unusable) {
7880
+ 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]);
7881
+ }
7882
+ const urlCandidates = [
7883
+ ...envUrlHit ? [envUrlHit] : [],
7884
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
7885
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
7886
+ ];
7887
+ const configuredUrl = urlCandidates[0] ?? null;
7888
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
7889
+ if (configuredUrl && divergentUrls.length > 0) {
7890
+ 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));
7891
+ }
7892
+ const warnings = [];
7893
+ if (configuredUrl && !envUrlHit) {
7894
+ 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.`);
7895
+ }
7896
+ const credential = resolveCredential(name, env, options.credentials);
7897
+ if (!credential) {
7898
+ const diskHint = credentialDiskSourcesForMessage(name, env);
7899
+ 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`;
7900
+ 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.`);
7901
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
7902
+ }
7903
+ if (credential.warning)
7904
+ warnings.push(credential.warning);
7905
+ let urlHit;
7906
+ if (configuredUrl) {
7907
+ urlHit = configuredUrl;
7908
+ } else {
7909
+ try {
7910
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
7911
+ } catch (error) {
7912
+ const message = error instanceof Error ? error.message : String(error);
7913
+ 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]]);
7914
+ }
7915
+ }
7916
+ const apiUrlSource = urlHit.key;
7917
+ let baseUrl;
7918
+ try {
7919
+ baseUrl = toV1BaseUrl(urlHit.value);
7920
+ } catch (error) {
7921
+ const message = error instanceof Error ? error.message : String(error);
7922
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
7923
+ }
7924
+ return {
7925
+ resolution: {
7926
+ transport: "http",
7927
+ transportSource: urlHit.key,
7928
+ baseUrl,
7929
+ apiUrlSource,
7930
+ apiKeyPresent: true,
7931
+ apiKeySource: credential.source,
7932
+ apiKeyTier: credential.tier,
7933
+ misconfigured: false,
7934
+ warning: warnings.length > 0 ? warnings.join(" ") : null
7935
+ },
7936
+ credential
7937
+ };
7938
+ }
7939
+ function resolveClientTransport(name, env = process.env, options = {}) {
7940
+ return resolveClientTransportSnapshot(name, env, options).resolution;
7941
+ }
7942
+ function credentialDiskSourcesForMessage(name, env) {
7943
+ const paths = credentialDiskSources(name, env);
7944
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
7945
+ }
7946
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7947
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
7948
+ "host",
7949
+ ":authority",
7950
+ "forwarded",
7951
+ "x-forwarded-host",
7952
+ "x-original-host"
7953
+ ]);
7954
+
7955
+ // src/lib/transport.ts
7956
+ init_config();
7957
+
7958
+ // src/lib/local-opt-in.ts
7959
+ var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
7960
+ function isHooksLocalOptIn(env = process.env) {
7961
+ return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
7962
+ }
7963
+ function hooksAuthorityEnvKeys() {
7964
+ const keys = clientTransportEnvKeys("hooks");
7965
+ return [
7966
+ ...keys.apiUrlKeys,
7967
+ ...keys.apiKeyKeys,
7968
+ credentialOverrideEnvKey("hooks"),
7969
+ credentialPointerEnvKey("hooks"),
7970
+ CREDENTIAL_PROFILE_ENV_KEY
7971
+ ];
7972
+ }
7973
+ function hasHooksEnvAuthorityIntent(env = process.env) {
7974
+ return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
7975
+ }
7976
+ function selectsHooksLocalStore(env = process.env) {
7977
+ return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
7978
+ }
7979
+ function hooksResolverEnv(env) {
7980
+ const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
7981
+ if (blanks.length === 0)
7982
+ return env;
7983
+ const next = { ...env };
7984
+ for (const key of blanks)
7985
+ delete next[key];
7986
+ return next;
7987
+ }
7988
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7989
+ function isAmbientHooksEnv(env) {
7990
+ if (typeof process !== "undefined" && env === process.env)
7991
+ return true;
7992
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
7993
+ }
7994
+ function hooksResolverInputs(env, credentials = {}) {
7995
+ const normalised = hooksResolverEnv(env);
7996
+ if (normalised === env)
7997
+ return { env: normalised, credentials };
7998
+ const keychain = { ...credentials.keychain };
7999
+ if (keychain.enabled === undefined && keychain.run === undefined) {
8000
+ keychain.enabled = isAmbientHooksEnv(env);
8001
+ }
8002
+ return { env: normalised, credentials: { ...credentials, keychain } };
8003
+ }
8004
+
8005
+ // src/lib/transport.ts
8006
+ var localNoticePrinted = false;
8007
+ function announceLocal(notice, reason) {
8008
+ if (localNoticePrinted)
8009
+ return;
8010
+ localNoticePrinted = true;
8011
+ 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.`;
8012
+ if (notice)
8013
+ notice(line);
8014
+ else if (typeof process !== "undefined")
8015
+ process.stderr.write(`${line}
8016
+ `);
8017
+ }
8018
+ function hooksRegistryOrigin(v1BaseUrl) {
8019
+ return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
8020
+ }
8021
+ function rethrowHooksAuthorityFailure(error) {
8022
+ const message = error instanceof Error ? error.message : String(error);
8023
+ const name = error instanceof Error ? error.name : "";
8024
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
8025
+ 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 });
8026
+ }
8027
+ if (/no API key could be resolved/.test(message)) {
8028
+ if (/is not set and no API key could be resolved/.test(message)) {
8029
+ 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 });
8030
+ }
8031
+ 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 });
8032
+ }
8033
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
8034
+ }
8035
+ function resolveHooksTransport(env = process.env, options = {}) {
8036
+ if (selectsHooksLocalStore(env)) {
8037
+ announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
8038
+ return { mode: "local", source: "local-opt-in", authority: null };
8039
+ }
8040
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8041
+ const tier1 = options.credentials?.apiKey;
8042
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
8043
+ let resolution;
8044
+ let credential;
8045
+ try {
8046
+ credential = resolveCredential("hooks", resolverEnv, credentialOptions);
8047
+ resolution = resolveClientTransport("hooks", resolverEnv, {
8048
+ credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
8049
+ });
8050
+ } catch (error) {
8051
+ rethrowHooksAuthorityFailure(error);
8052
+ }
8053
+ if (!credential) {
8054
+ throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
8055
+ }
8056
+ return {
8057
+ mode: "remote",
8058
+ source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
8059
+ authority: {
8060
+ origin: hooksRegistryOrigin(resolution.baseUrl),
8061
+ apiKey: credential.apiKey,
8062
+ apiUrlSource: resolution.apiUrlSource,
8063
+ apiKeySource: credential.source,
8064
+ apiKeyTier: credential.tier,
8065
+ v1BaseUrl: resolution.baseUrl,
8066
+ warning: resolution.warning
8067
+ }
8068
+ };
8069
+ }
8070
+ function resolveHooksServePublishKey(env = process.env, options = {}) {
8071
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8072
+ const tier1 = options.credentials?.apiKey;
8073
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
8074
+ return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
8075
+ }
8076
+
8077
+ // src/index.ts
7543
8078
  init_store();
7544
8079
  init_manifest();
7545
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
7546
8080
 
7547
8081
  // src/lib/custom-install.ts
7548
8082
  init_config();
7549
8083
  init_manifest();
7550
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7551
- import { join as join11 } from "path";
8084
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
8085
+ import { join as join10 } from "path";
7552
8086
  import { tmpdir } from "os";
7553
8087
  function manifestAt(dir) {
7554
- const path = join11(dir, "manifest.json");
7555
- if (!existsSync10(path))
8088
+ const path = join10(dir, "manifest.json");
8089
+ if (!existsSync9(path))
7556
8090
  return;
7557
8091
  try {
7558
- return parseManifest(readFileSync8(path, "utf-8"));
8092
+ return parseManifest(readFileSync7(path, "utf-8"));
7559
8093
  } catch {
7560
8094
  return;
7561
8095
  }
@@ -7574,8 +8108,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7574
8108
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7575
8109
  }
7576
8110
  async function cloneToTemp(gitUrl) {
7577
- const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
7578
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
8111
+ const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
8112
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
7579
8113
  stdout: "pipe",
7580
8114
  stderr: "pipe"
7581
8115
  });
@@ -7585,10 +8119,10 @@ async function cloneToTemp(gitUrl) {
7585
8119
  proc.exited
7586
8120
  ]);
7587
8121
  if (exitCode !== 0) {
7588
- rmSync3(tmp, { recursive: true, force: true });
8122
+ rmSync2(tmp, { recursive: true, force: true });
7589
8123
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7590
8124
  }
7591
- return join11(tmp, "repo");
8125
+ return join10(tmp, "repo");
7592
8126
  }
7593
8127
  function isGitUrl(value) {
7594
8128
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7598,7 +8132,7 @@ function isCustomSource(value) {
7598
8132
  return true;
7599
8133
  if (value.startsWith("http://") || value.startsWith("https://"))
7600
8134
  return true;
7601
- return existsSync10(value);
8135
+ return existsSync9(value);
7602
8136
  }
7603
8137
  async function installCustomSource(source) {
7604
8138
  if (isGitUrl(source)) {
@@ -7606,17 +8140,17 @@ async function installCustomSource(source) {
7606
8140
  try {
7607
8141
  return copyManifestDir(repoDir, source, "git");
7608
8142
  } finally {
7609
- rmSync3(repoDir, { recursive: true, force: true });
8143
+ rmSync2(repoDir, { recursive: true, force: true });
7610
8144
  }
7611
8145
  }
7612
8146
  if (source.startsWith("http://") || source.startsWith("https://")) {
7613
8147
  return installManifestUrl(source);
7614
8148
  }
7615
- if (!existsSync10(source)) {
8149
+ if (!existsSync9(source)) {
7616
8150
  throw new Error(`No such local path: ${source}`);
7617
8151
  }
7618
8152
  if (source.endsWith("manifest.json")) {
7619
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8153
+ const manifest = parseManifest(readFileSync7(source, "utf-8"));
7620
8154
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7621
8155
  }
7622
8156
  return copyManifestDir(source, source, "local");
@@ -7647,8 +8181,7 @@ async function installManifestUrl(url) {
7647
8181
  }
7648
8182
  // src/lib/sync.ts
7649
8183
  import { createHash as createHash2 } from "crypto";
7650
- import { readFileSync as readFileSync9 } from "fs";
7651
- init_config();
8184
+ import { readFileSync as readFileSync8 } from "fs";
7652
8185
  init_db();
7653
8186
  init_store();
7654
8187
  init_manifest();
@@ -7656,7 +8189,7 @@ function sha256OfText(text) {
7656
8189
  return createHash2("sha256").update(text).digest("hex");
7657
8190
  }
7658
8191
  function sha256OfPath(path) {
7659
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8192
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
7660
8193
  }
7661
8194
  function collectBundledCatalog() {
7662
8195
  const out = [];
@@ -7672,9 +8205,8 @@ function collectBundledCatalog() {
7672
8205
  }
7673
8206
  return out;
7674
8207
  }
7675
- async function fetchJson(base, path, opts) {
8208
+ async function fetchJson(base, path, apiKey, opts) {
7676
8209
  const headers = { accept: "application/json" };
7677
- const apiKey = resolveApiKey();
7678
8210
  if (apiKey)
7679
8211
  headers["x-api-key"] = apiKey;
7680
8212
  const res = await fetch(`${base}${path}`, {
@@ -7683,7 +8215,7 @@ async function fetchJson(base, path, opts) {
7683
8215
  signal: AbortSignal.timeout(15000)
7684
8216
  });
7685
8217
  if (res.status === 401) {
7686
- throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
8218
+ 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)");
7687
8219
  }
7688
8220
  if (!res.ok) {
7689
8221
  throw new Error(`GET ${path} failed with status ${res.status}`);
@@ -7694,9 +8226,9 @@ async function fetchJson(base, path, opts) {
7694
8226
  }
7695
8227
  return body;
7696
8228
  }
7697
- async function fetchRemoteState(apiUrl) {
7698
- const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
7699
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8229
+ async function fetchRemoteState(apiUrl, apiKey) {
8230
+ const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
8231
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7700
8232
  if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
7701
8233
  throw new Error("remote catalog is malformed: missing hooks array");
7702
8234
  }
@@ -7739,20 +8271,25 @@ function computeDiff(catalog, remoteLock) {
7739
8271
  return diff;
7740
8272
  }
7741
8273
  async function planSync(options = {}) {
7742
- const apiUrl = resolveApiUrl();
7743
- if (apiUrl) {
7744
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
7745
- return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
8274
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8275
+ credentials: options.credentials
8276
+ });
8277
+ if (transport.mode === "remote" && transport.authority) {
8278
+ const { origin, apiKey } = transport.authority;
8279
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8280
+ return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
7746
8281
  }
7747
8282
  const diff = computeDiff(collectBundledCatalog(), null);
7748
8283
  return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
7749
8284
  }
7750
8285
  async function syncHooks(options = {}) {
7751
- const apiUrl = resolveApiUrl();
7752
- const plan = await planSync();
8286
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8287
+ credentials: options.credentials
8288
+ });
8289
+ const plan = await planSync(options);
7753
8290
  if (options.dryRun)
7754
8291
  return { ...plan, dryRun: true };
7755
- if (!apiUrl) {
8292
+ if (transport.mode !== "remote" || !transport.authority) {
7756
8293
  const db = getDb();
7757
8294
  for (const entry of collectBundledCatalog()) {
7758
8295
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
@@ -7767,19 +8304,20 @@ async function syncHooks(options = {}) {
7767
8304
  }
7768
8305
  return plan;
7769
8306
  }
7770
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
8307
+ const { origin, apiKey } = transport.authority;
8308
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
7771
8309
  const diff = computeDiff(catalog, remoteLock);
7772
- const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
7773
- await commitSyncArtifacts(staged, apiUrl, remoteLock);
7774
- return { apiUrl, dryRun: false, diff };
8310
+ const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
8311
+ await commitSyncArtifacts(staged, origin, remoteLock);
8312
+ return { apiUrl: origin, dryRun: false, diff };
7775
8313
  }
7776
- async function stageSyncArtifacts(apiUrl, names, remoteLock) {
8314
+ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
7777
8315
  const staged = [];
7778
8316
  for (const name of names) {
7779
8317
  const entry = remoteLock.hooks[name];
7780
8318
  if (!entry)
7781
8319
  continue;
7782
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
8320
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
7783
8321
  if (!artifact.manifest || typeof artifact.script !== "string") {
7784
8322
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
7785
8323
  }
@@ -7846,8 +8384,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7846
8384
  throw err;
7847
8385
  }
7848
8386
  }
7849
- async function fetchPinnedHook(name, version, apiUrl) {
7850
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8387
+ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
8388
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7851
8389
  const entry = remoteLock.hooks?.[name];
7852
8390
  if (!entry) {
7853
8391
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
@@ -7856,7 +8394,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
7856
8394
  if (!isLatest && !(entry.versions ?? []).includes(version)) {
7857
8395
  throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
7858
8396
  }
7859
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
8397
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
7860
8398
  if (!artifact.manifest || typeof artifact.script !== "string") {
7861
8399
  throw new Error(`artifact for '${name}@${version}' is malformed`);
7862
8400
  }
@@ -7910,9 +8448,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
7910
8448
  // src/serve.ts
7911
8449
  init_manifest();
7912
8450
  init_store();
7913
- init_config();
7914
- import { readFileSync as readFileSync10 } from "fs";
7915
- import { dirname as dirname4, join as join12 } from "path";
8451
+ import { readFileSync as readFileSync9 } from "fs";
8452
+ import { dirname as dirname4, join as join11 } from "path";
7916
8453
  import { fileURLToPath as fileURLToPath3 } from "url";
7917
8454
 
7918
8455
  // src/lib/secure-compare.ts
@@ -8073,7 +8610,7 @@ var openApiDocument = {
8073
8610
  apiKey: {
8074
8611
  type: "http",
8075
8612
  scheme: "bearer",
8076
- description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
8613
+ 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."
8077
8614
  }
8078
8615
  }
8079
8616
  }
@@ -8086,8 +8623,8 @@ var SERVE_HOST = "127.0.0.1";
8086
8623
  var SERVE_SERVICE_NAME = "hooks-registry";
8087
8624
  function packageVersion() {
8088
8625
  try {
8089
- const pkgPath = join12(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8090
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
8626
+ const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8627
+ return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
8091
8628
  } catch {
8092
8629
  return "0.0.0";
8093
8630
  }
@@ -8244,12 +8781,11 @@ function resolveServeOptions(options) {
8244
8781
  }
8245
8782
  function startServeServer(options) {
8246
8783
  const { port, host } = resolveServeOptions(options);
8247
- const apiKey = resolveApiKey();
8248
8784
  const server = Bun.serve({
8249
8785
  hostname: host,
8250
8786
  port,
8251
8787
  async fetch(req) {
8252
- return handleServeRequest(req, apiKey);
8788
+ return handleServeRequest(req, resolveHooksServePublishKey());
8253
8789
  }
8254
8790
  });
8255
8791
  console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
@@ -8354,11 +8890,11 @@ async function runHook(name, input, options = {}) {
8354
8890
  throw new Error(`Hook '${name}' not found`);
8355
8891
  const hookScript = getHookPath(name) + "/src/hook.ts";
8356
8892
  const resolvedScript = custom2?.scriptPath;
8357
- const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
8893
+ const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
8358
8894
  if (!script)
8359
8895
  throw new Error(`Hook script not found: ${name}`);
8360
8896
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
8361
- const content = readFileSync11(script);
8897
+ const content = readFileSync10(script);
8362
8898
  const check = checkScriptHash2(name, sha256Of2(content));
8363
8899
  if (!check.ok) {
8364
8900
  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.`);
@@ -8449,7 +8985,6 @@ async function runHook(name, input, options = {}) {
8449
8985
  export {
8450
8986
  writeLock,
8451
8987
  writeCustomHook,
8452
- writeConfig,
8453
8988
  verifyScriptHash,
8454
8989
  upsertHookRecord,
8455
8990
  updateProfile,
@@ -8470,11 +9005,11 @@ export {
8470
9005
  resolveTables,
8471
9006
  resolveScriptPath,
8472
9007
  resolveScript,
9008
+ resolveHooksTransport,
9009
+ resolveHooksServePublishKey,
8473
9010
  resolveHookMeta,
8474
9011
  resolveHookDir,
8475
9012
  resolveHook,
8476
- resolveApiUrl,
8477
- resolveApiKey,
8478
9013
  removeProjectHook,
8479
9014
  removePinnedHook,
8480
9015
  removeHookRecord,
@@ -8482,7 +9017,6 @@ export {
8482
9017
  removeHook,
8483
9018
  readLock,
8484
9019
  readCustomManifest,
8485
- readConfig,
8486
9020
  provisionCloudflareResources,
8487
9021
  planSync,
8488
9022
  pinInstalledHook,
@@ -8500,6 +9034,7 @@ export {
8500
9034
  installHook,
8501
9035
  installCustomSource,
8502
9036
  importProfiles,
9037
+ hooksRegistryOrigin,
8503
9038
  hookExists,
8504
9039
  handleServeRequest,
8505
9040
  getSyncMetaAll,
@@ -8524,7 +9059,6 @@ export {
8524
9059
  getHookEvents,
8525
9060
  getHook,
8526
9061
  getCustomHooksDir,
8527
- getConfigPath,
8528
9062
  fetchPinnedHook,
8529
9063
  exportProfiles,
8530
9064
  deleteProfile,