@hasna/hooks 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +32 -6
  2. package/bin/index.js +1872 -1300
  3. package/bin/serve.js +730 -551
  4. package/dist/config.d.ts +25 -19
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +1198 -712
  7. package/dist/lib/app-home.d.ts +9 -0
  8. package/dist/lib/installer.d.ts +0 -9
  9. package/dist/lib/local-opt-in.d.ts +80 -0
  10. package/dist/lib/profiles.d.ts +10 -0
  11. package/dist/lib/resolver-types.d.ts +45 -0
  12. package/dist/lib/sync.d.ts +13 -8
  13. package/dist/lib/transport.d.ts +87 -0
  14. package/dist/openapi.d.ts +1 -1
  15. package/dist/storage.js +54 -63
  16. package/hooks/hook-affected-tests/package.json +1 -1
  17. package/hooks/hook-agent-rules-version-check/package.json +1 -1
  18. package/hooks/hook-agentmessages/package.json +1 -1
  19. package/hooks/hook-announce-start/package.json +1 -1
  20. package/hooks/hook-announce-stop/package.json +1 -1
  21. package/hooks/hook-autoformat/package.json +1 -1
  22. package/hooks/hook-branchprotect/package.json +1 -1
  23. package/hooks/hook-checkbugs/package.json +1 -1
  24. package/hooks/hook-checkdocs/package.json +1 -1
  25. package/hooks/hook-checkfiles/package.json +1 -1
  26. package/hooks/hook-checklint/package.json +1 -1
  27. package/hooks/hook-checkpoint/package.json +1 -1
  28. package/hooks/hook-checksecurity/package.json +1 -1
  29. package/hooks/hook-checktasks/package.json +1 -1
  30. package/hooks/hook-checktests/package.json +1 -1
  31. package/hooks/hook-conflict-detect/package.json +1 -1
  32. package/hooks/hook-contextrefresh/package.json +1 -1
  33. package/hooks/hook-desktopnotify/package.json +1 -1
  34. package/hooks/hook-dm-inject/package.json +1 -1
  35. package/hooks/hook-envsetup/package.json +1 -1
  36. package/hooks/hook-failure-to-task/package.json +1 -1
  37. package/hooks/hook-filelock/package.json +1 -1
  38. package/hooks/hook-fleet-blockers-gate/package.json +1 -1
  39. package/hooks/hook-fleet-catchup/package.json +1 -1
  40. package/hooks/hook-gitguard/package.json +1 -1
  41. package/hooks/hook-packageage/package.json +1 -1
  42. package/hooks/hook-permissionguard/package.json +1 -1
  43. package/hooks/hook-phonenotify/package.json +1 -1
  44. package/hooks/hook-precompact/package.json +1 -1
  45. package/hooks/hook-protectfiles/package.json +1 -1
  46. package/hooks/hook-stylescheck/package.json +1 -1
  47. package/hooks/hook-typecheck-gate/package.json +1 -1
  48. package/package.json +4 -9
  49. package/scripts/ensure-profiles-dir.mjs +52 -1
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
  }
@@ -6871,21 +6474,21 @@ function getHook(name) {
6871
6474
  return HOOKS.find((h) => h.name === name);
6872
6475
  }
6873
6476
  // 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";
6477
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
6478
+ import { join as join7, dirname as dirname3 } from "path";
6479
+ import { homedir as homedir3 } from "os";
6877
6480
  import { fileURLToPath as fileURLToPath2 } from "url";
6878
6481
 
6879
6482
  // src/lib/resolve.ts
6880
- import { dirname as dirname2, join as join8 } from "path";
6881
- import { existsSync as existsSync6 } from "fs";
6483
+ import { dirname as dirname2, join as join6 } from "path";
6484
+ import { existsSync as existsSync5 } from "fs";
6882
6485
  import { fileURLToPath } from "url";
6883
6486
  init_manifest();
6884
6487
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
6885
- const sourceLayout = join8(moduleDir, "..", "..", "hooks");
6886
- if (existsSync6(join8(sourceLayout, "hook-gitguard")))
6488
+ const sourceLayout = join6(moduleDir, "..", "..", "hooks");
6489
+ if (existsSync5(join6(sourceLayout, "hook-gitguard")))
6887
6490
  return sourceLayout;
6888
- return join8(moduleDir, "..", "hooks");
6491
+ return join6(moduleDir, "..", "hooks");
6889
6492
  }
6890
6493
  function resolveHookDir(name) {
6891
6494
  const custom2 = readCustomManifest(name);
@@ -6896,11 +6499,11 @@ function resolveHookDir(name) {
6896
6499
  return;
6897
6500
  const short = shortManifestName(name);
6898
6501
  const bundledBase = resolveBundledHooksDir();
6899
- const direct = join8(bundledBase, short);
6900
- if (existsSync6(direct))
6502
+ const direct = join6(bundledBase, short);
6503
+ if (existsSync5(direct))
6901
6504
  return direct;
6902
- const prefixed = join8(bundledBase, `hook-${short}`);
6903
- return existsSync6(prefixed) ? prefixed : undefined;
6505
+ const prefixed = join6(bundledBase, `hook-${short}`);
6506
+ return existsSync5(prefixed) ? prefixed : undefined;
6904
6507
  }
6905
6508
  function resolveScriptPath(name) {
6906
6509
  const custom2 = readCustomManifest(name);
@@ -6909,8 +6512,8 @@ function resolveScriptPath(name) {
6909
6512
  const dir = resolveHookDir(name);
6910
6513
  if (!dir)
6911
6514
  return;
6912
- const script = join8(dir, "src", "hook.ts");
6913
- return existsSync6(script) ? script : undefined;
6515
+ const script = join6(dir, "src", "hook.ts");
6516
+ return existsSync5(script) ? script : undefined;
6914
6517
  }
6915
6518
  function resolveHookMeta(name) {
6916
6519
  const custom2 = readCustomManifest(name);
@@ -6965,7 +6568,7 @@ function resolveHook(name) {
6965
6568
  init_manifest();
6966
6569
  init_store();
6967
6570
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
6968
- var HOOKS_DIR = existsSync8(join9(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join9(__dirname2, "..", "..", "hooks") : join9(__dirname2, "..", "hooks");
6571
+ var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
6969
6572
  function normalizeHookName(name) {
6970
6573
  return name.startsWith("hook-") ? name : `hook-${name}`;
6971
6574
  }
@@ -7035,25 +6638,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
7035
6638
  return globalOverride;
7036
6639
  const dir = getTargetSettingsDir(target);
7037
6640
  if (scope === "project") {
7038
- return target === "codewith" ? join9(process.cwd(), dir, "config.toml") : join9(process.cwd(), dir, "settings.json");
6641
+ return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
7039
6642
  }
7040
- return target === "codewith" ? join9(homedir4(), dir, "config.toml") : join9(homedir4(), dir, "settings.json");
6643
+ return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
7041
6644
  }
7042
6645
  function getHookPath(name) {
7043
6646
  const shortName = shortHookName(name);
7044
6647
  const resolved = resolveHookDir(shortName);
7045
6648
  if (resolved)
7046
6649
  return resolved;
7047
- return join9(HOOKS_DIR, normalizeHookName(shortName));
6650
+ return join7(HOOKS_DIR, normalizeHookName(shortName));
7048
6651
  }
7049
6652
  function hookExists(name) {
7050
- return existsSync8(getHookPath(name));
6653
+ return existsSync7(getHookPath(name));
7051
6654
  }
7052
6655
  function readSettings(scope = "global", target = "claude") {
7053
6656
  const path = getSettingsPath(scope, target);
7054
6657
  try {
7055
- if (existsSync8(path)) {
7056
- return JSON.parse(readFileSync6(path, "utf-8"));
6658
+ if (existsSync7(path)) {
6659
+ return JSON.parse(readFileSync4(path, "utf-8"));
7057
6660
  }
7058
6661
  } catch (error) {
7059
6662
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -7063,10 +6666,10 @@ function readSettings(scope = "global", target = "claude") {
7063
6666
  function writeSettings(settings, scope = "global", target = "claude") {
7064
6667
  const path = getSettingsPath(scope, target);
7065
6668
  const dir = dirname3(path);
7066
- if (!existsSync8(dir)) {
7067
- mkdirSync5(dir, { recursive: true });
6669
+ if (!existsSync7(dir)) {
6670
+ mkdirSync4(dir, { recursive: true });
7068
6671
  }
7069
- writeFileSync4(path, JSON.stringify(settings, null, 2) + `
6672
+ writeFileSync3(path, JSON.stringify(settings, null, 2) + `
7070
6673
  `);
7071
6674
  }
7072
6675
  function getTargetEventName(internalEvent, target) {
@@ -7152,7 +6755,7 @@ function buildCodewithTomlFragment(name, profile) {
7152
6755
  function readCodewithConfig(scope, configPath) {
7153
6756
  const path = getSettingsPath(scope, "codewith", configPath);
7154
6757
  try {
7155
- return existsSync8(path) ? readFileSync6(path, "utf-8") : "";
6758
+ return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7156
6759
  } catch {
7157
6760
  return "";
7158
6761
  }
@@ -7160,13 +6763,13 @@ function readCodewithConfig(scope, configPath) {
7160
6763
  function appendCodewithFragment(fragment, scope, configPath) {
7161
6764
  const path = getSettingsPath(scope, "codewith", configPath);
7162
6765
  const dir = dirname3(path);
7163
- if (!existsSync8(dir))
7164
- mkdirSync5(dir, { recursive: true });
7165
- const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
6766
+ if (!existsSync7(dir))
6767
+ mkdirSync4(dir, { recursive: true });
6768
+ const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7166
6769
  const sep2 = existing.trim() ? `
7167
6770
 
7168
6771
  ` : "";
7169
- writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6772
+ writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
7170
6773
  return path;
7171
6774
  }
7172
6775
  function detectConflict(name, scope, target) {
@@ -7463,16 +7066,30 @@ function codewithHasHookEntryInText(configText, name) {
7463
7066
  }
7464
7067
  function codewithHasHookEntry(name, scope = "global") {
7465
7068
  const path = getSettingsPath(scope, "codewith");
7466
- if (!existsSync8(path))
7069
+ if (!existsSync7(path))
7467
7070
  return false;
7468
7071
  try {
7469
- return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
7072
+ return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
7470
7073
  } catch {
7471
7074
  return false;
7472
7075
  }
7473
7076
  }
7474
- function uninstallHook(name, scope = "global", target = "claude") {
7475
- const shortName = shortHookName(name);
7077
+ function removeStoreDirFailClosed(dir) {
7078
+ const entries = readdirSync3(dir, { withFileTypes: true });
7079
+ for (const entry of entries) {
7080
+ if (entry.isDirectory()) {
7081
+ removeStoreDirFailClosed(join7(dir, entry.name));
7082
+ }
7083
+ }
7084
+ for (const entry of entries) {
7085
+ if (!entry.isDirectory()) {
7086
+ unlinkSync(join7(dir, entry.name));
7087
+ }
7088
+ }
7089
+ rmdirSync(dir);
7090
+ }
7091
+ function uninstallHook(name, scope = "global", target = "claude") {
7092
+ const shortName = shortHookName(name);
7476
7093
  const custom2 = readCustomManifest(shortName);
7477
7094
  const bundledMeta = custom2 ? undefined : getHook(shortName);
7478
7095
  const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
@@ -7497,11 +7114,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
7497
7114
  const registrationsRemaining = [];
7498
7115
  if (target === "codewith" || target === "all") {
7499
7116
  const codewithPath = getSettingsPath(scope, "codewith");
7500
- if (existsSync8(codewithPath)) {
7501
- const before = readFileSync6(codewithPath, "utf-8");
7117
+ if (existsSync7(codewithPath)) {
7118
+ const before = readFileSync4(codewithPath, "utf-8");
7502
7119
  const after = removeCodewithHookEntry(before, shortName);
7503
7120
  if (after.removed) {
7504
- writeFileSync4(codewithPath, after.text, "utf-8");
7121
+ writeFileSync3(codewithPath, after.text, "utf-8");
7505
7122
  }
7506
7123
  if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
7507
7124
  registrationsRemaining.push("codewith");
@@ -7510,7 +7127,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
7510
7127
  }
7511
7128
  if (custom2) {
7512
7129
  try {
7513
- rmSync(customHookDir(shortName), { recursive: true, force: true });
7130
+ removeStoreDirFailClosed(customHookDir(shortName));
7514
7131
  } catch {
7515
7132
  return {
7516
7133
  name: shortName,
@@ -7540,22 +7157,891 @@ function uninstallHook(name, scope = "global", target = "claude") {
7540
7157
  // src/index.ts
7541
7158
  init_profiles();
7542
7159
  init_config();
7160
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
7161
+
7162
+ // ../contracts/dist/client/transport.js
7163
+ import { isIP } from "net";
7164
+ import { spawnSync } from "child_process";
7165
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
7166
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
7167
+ import { createRequire } from "module";
7168
+ import { hostname as osHostname } from "os";
7169
+ import { isAbsolute as isAbsolute2, join as join9 } from "path";
7170
+ function envToken(name) {
7171
+ return name.toUpperCase().replace(/-/g, "_");
7172
+ }
7173
+ function clientTransportEnvKeys(name) {
7174
+ const envSegment = envToken(name);
7175
+ return {
7176
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
7177
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
7178
+ };
7179
+ }
7180
+ function credentialOverrideEnvKey(name) {
7181
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
7182
+ }
7183
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
7184
+ function credentialPointerEnvKey(name) {
7185
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
7186
+ }
7187
+
7188
+ class CredentialResolutionError extends Error {
7189
+ appName;
7190
+ attempted;
7191
+ constructor(appName, message, attempted) {
7192
+ super(message);
7193
+ this.name = "CredentialResolutionError";
7194
+ this.appName = appName;
7195
+ this.attempted = attempted;
7196
+ }
7197
+ }
7198
+
7199
+ class CredentialFileUnsafeError extends Error {
7200
+ path;
7201
+ constructor(path, reason) {
7202
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
7203
+ this.name = "CredentialFileUnsafeError";
7204
+ this.path = path;
7205
+ }
7206
+ }
7207
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
7208
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
7209
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
7210
+ var HASNA_HOME_DIR = ".hasna";
7211
+ var CONFIG_SUBDIR = "config";
7212
+ var CREDENTIALS_FILE = "credentials";
7213
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
7214
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
7215
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
7216
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
7217
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
7218
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7219
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
7220
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
7221
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
7222
+ function homeDir(env) {
7223
+ const home = env.HOME?.trim();
7224
+ return home ? home : null;
7225
+ }
7226
+ function absoluteOverride(env, key) {
7227
+ const value = env[key]?.trim();
7228
+ return value && isAbsolute2(value) ? value : null;
7229
+ }
7230
+ function hasnaHomeDir(env) {
7231
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
7232
+ if (override)
7233
+ return override;
7234
+ const home = homeDir(env);
7235
+ return home ? join9(home, HASNA_HOME_DIR) : null;
7236
+ }
7237
+ function appConfigDir(name, env) {
7238
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
7239
+ if (configRoot)
7240
+ return join9(configRoot, name);
7241
+ const root = hasnaHomeDir(env);
7242
+ return root ? join9(root, name, CONFIG_SUBDIR) : null;
7243
+ }
7244
+ function credentialDiskSourceList(name, env, profile = null) {
7245
+ if (!SAFE_APP_SLUG.test(name))
7246
+ return [];
7247
+ const directory = appConfigDir(name, env);
7248
+ if (!directory)
7249
+ return [];
7250
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
7251
+ return [{ path: join9(directory, file), tier: "disk" }];
7252
+ }
7253
+ function credentialDiskSources(name, env) {
7254
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
7255
+ }
7256
+ function profileDiskSources(name, env, profile) {
7257
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
7258
+ }
7259
+ function parseEnvFile(text) {
7260
+ const values = new Map;
7261
+ const unusable = new Set;
7262
+ for (const rawLine of text.split(/\r?\n/)) {
7263
+ const line = rawLine.trim();
7264
+ if (line.length === 0 || line.startsWith("#"))
7265
+ continue;
7266
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
7267
+ const equals = withoutExport.indexOf("=");
7268
+ if (equals <= 0)
7269
+ continue;
7270
+ const key = withoutExport.slice(0, equals).trim();
7271
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
7272
+ continue;
7273
+ let value = withoutExport.slice(equals + 1).trim();
7274
+ const quote = value[0];
7275
+ if (quote === '"' || quote === "'") {
7276
+ if (value.length < 2 || !value.endsWith(quote)) {
7277
+ unusable.add(key);
7278
+ continue;
7279
+ }
7280
+ value = value.slice(1, -1);
7281
+ }
7282
+ if (value.trim().length === 0) {
7283
+ unusable.add(key);
7284
+ continue;
7285
+ }
7286
+ if (values.has(key) && values.get(key) !== value)
7287
+ unusable.add(key);
7288
+ values.set(key, value);
7289
+ }
7290
+ return { values, unusable };
7291
+ }
7292
+ function configFileModeAllowed(mode) {
7293
+ const permissions = mode & 4095;
7294
+ return permissions === 256 || permissions === 384;
7295
+ }
7296
+ function configFileReadsCoherent(before, after) {
7297
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
7298
+ }
7299
+ function readAppConfigFile(path) {
7300
+ const unsafe = (reason) => {
7301
+ throw new CredentialFileUnsafeError(path, reason);
7302
+ };
7303
+ let fd = -1;
7304
+ try {
7305
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
7306
+ } catch (error) {
7307
+ const code = error.code;
7308
+ if (code === "ENOENT" || code === "ENOTDIR")
7309
+ return null;
7310
+ if (code === "ELOOP")
7311
+ unsafe("the path is a symlink");
7312
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
7313
+ }
7314
+ try {
7315
+ const before = fstatSync(fd);
7316
+ if (!before.isFile())
7317
+ unsafe("the path is not a regular file");
7318
+ if (!configFileModeAllowed(before.mode)) {
7319
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
7320
+ }
7321
+ const uid = process.getuid?.() ?? process.geteuid?.();
7322
+ if (uid !== undefined && before.uid !== uid)
7323
+ unsafe("the file is not owned by the current user");
7324
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
7325
+ unsafe("the file exceeds the size limit");
7326
+ const bytes = readFileSync6(fd);
7327
+ const after = fstatSync(fd);
7328
+ if (!configFileReadsCoherent(before, after)) {
7329
+ unsafe("the file changed while being read");
7330
+ }
7331
+ return parseEnvFile(bytes.toString("utf8"));
7332
+ } finally {
7333
+ if (fd !== -1)
7334
+ closeSync(fd);
7335
+ }
7336
+ }
7337
+ function readCredentialFile(path, apiKeyKeys) {
7338
+ const parsed = readAppConfigFile(path);
7339
+ if (!parsed)
7340
+ return null;
7341
+ for (const key of apiKeyKeys) {
7342
+ if (parsed.unusable.has(key)) {
7343
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
7344
+ }
7345
+ }
7346
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7347
+ if (new Set(values).size > 1) {
7348
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
7349
+ }
7350
+ return values[0] ?? null;
7351
+ }
7352
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
7353
+ function appConfigDiskValue(name, env, keys) {
7354
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
7355
+ if (wanted.length === 0)
7356
+ return null;
7357
+ for (const path of credentialDiskSources(name, env)) {
7358
+ const parsed = readAppConfigFile(path);
7359
+ if (!parsed)
7360
+ continue;
7361
+ if (wanted.some((key) => parsed.unusable.has(key))) {
7362
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
7363
+ }
7364
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7365
+ if (new Set(values).size > 1)
7366
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
7367
+ for (const key of wanted) {
7368
+ if (parsed.unusable.has(key))
7369
+ return { key, value: "", path, unusable: true };
7370
+ const value = parsed.values.get(key)?.trim();
7371
+ if (value)
7372
+ return { key, value, path };
7373
+ }
7374
+ }
7375
+ return null;
7376
+ }
7377
+ function assertUsableCredential(appName, source, value) {
7378
+ if (VAULT_POINTER_SHAPE.test(value)) {
7379
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
7380
+ }
7381
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
7382
+ return;
7383
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
7384
+ }
7385
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
7386
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
7387
+ function sealCredential(fields) {
7388
+ const { apiKey } = fields;
7389
+ const visible = {
7390
+ tier: fields.tier,
7391
+ source: fields.source,
7392
+ deliberate: fields.deliberate,
7393
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
7394
+ warning: fields.warning
7395
+ };
7396
+ const sealed = { ...visible };
7397
+ Object.defineProperty(sealed, "apiKey", {
7398
+ value: apiKey,
7399
+ enumerable: false,
7400
+ writable: false,
7401
+ configurable: false
7402
+ });
7403
+ if (fields.pointerVaultKey !== undefined) {
7404
+ Object.defineProperty(sealed, "pointerVaultKey", {
7405
+ value: fields.pointerVaultKey,
7406
+ enumerable: false,
7407
+ writable: false,
7408
+ configurable: false
7409
+ });
7410
+ }
7411
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
7412
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
7413
+ enumerable: false,
7414
+ writable: false,
7415
+ configurable: false
7416
+ });
7417
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
7418
+ value: true,
7419
+ enumerable: false,
7420
+ writable: false,
7421
+ configurable: false
7422
+ });
7423
+ return Object.freeze(sealed);
7424
+ }
7425
+ function firstEnvValue(env, keys) {
7426
+ for (const key of keys) {
7427
+ if (!Object.prototype.hasOwnProperty.call(env, key))
7428
+ continue;
7429
+ const value = env[key]?.trim();
7430
+ if (value)
7431
+ return { key, value };
7432
+ }
7433
+ return null;
7434
+ }
7435
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7436
+ function isAmbientEnvironment(env) {
7437
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
7438
+ }
7439
+ function defaultKeychainRunner(argv) {
7440
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
7441
+ encoding: "utf8",
7442
+ stdio: ["ignore", "pipe", "pipe"],
7443
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
7444
+ });
7445
+ return {
7446
+ status: result.status,
7447
+ stdout: result.stdout ?? "",
7448
+ stderr: result.error ? result.error.message : result.stderr ?? ""
7449
+ };
7450
+ }
7451
+ function keychainTierEnabled(env, options) {
7452
+ if ((options.platform ?? process.platform) !== "darwin")
7453
+ return false;
7454
+ if (options.enabled !== undefined)
7455
+ return options.enabled;
7456
+ return options.run !== undefined || isAmbientEnvironment(env);
7457
+ }
7458
+ function keychainAccount(env, options) {
7459
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
7460
+ if (station)
7461
+ return station;
7462
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
7463
+ if (host)
7464
+ return host;
7465
+ const user = env.USER?.trim();
7466
+ return user || null;
7467
+ }
7468
+ function keychainFailureHint(text) {
7469
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
7470
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
7471
+ return clean ? `: ${clean}` : "";
7472
+ }
7473
+ function readKeychainItem(name, env, kind, options) {
7474
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
7475
+ return null;
7476
+ const account = keychainAccount(env, options);
7477
+ if (!account)
7478
+ return null;
7479
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
7480
+ const source = `keychain:${service}@${account}`;
7481
+ const run = options.run ?? defaultKeychainRunner;
7482
+ let result;
7483
+ try {
7484
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
7485
+ } catch (error) {
7486
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
7487
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
7488
+ }
7489
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
7490
+ return null;
7491
+ if (result.status !== 0) {
7492
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
7493
+ }
7494
+ const value = result.stdout.trim();
7495
+ if (!value) {
7496
+ throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
7497
+ }
7498
+ return { value, source };
7499
+ }
7500
+ function keychainConfigValue(name, env, options = {}) {
7501
+ return readKeychainItem(name, env, "api-url", options);
7502
+ }
7503
+ function snapshotClientEnvironment(name, env) {
7504
+ const keys = clientTransportEnvKeys(name);
7505
+ const ambient = isAmbientEnvironment(env);
7506
+ const snapshot = Object.create(null);
7507
+ for (const key of [
7508
+ ...keys.apiUrlKeys,
7509
+ ...keys.apiKeyKeys,
7510
+ credentialOverrideEnvKey(name),
7511
+ credentialPointerEnvKey(name),
7512
+ CREDENTIAL_PROFILE_ENV_KEY,
7513
+ "HOME",
7514
+ HASNA_HOME_ENV_KEY,
7515
+ HASNA_CONFIG_HOME_ENV_KEY,
7516
+ KEYCHAIN_STATION_ENV_KEY,
7517
+ "USER"
7518
+ ]) {
7519
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
7520
+ if (!descriptor)
7521
+ continue;
7522
+ if (!("value" in descriptor)) {
7523
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
7524
+ }
7525
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
7526
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
7527
+ }
7528
+ snapshot[key] = descriptor.value;
7529
+ }
7530
+ if (ambient) {
7531
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
7532
+ value: true,
7533
+ enumerable: false,
7534
+ writable: false,
7535
+ configurable: false
7536
+ });
7537
+ }
7538
+ return Object.freeze(snapshot);
7539
+ }
7540
+ function resolveCredential(name, env, options = {}) {
7541
+ env = snapshotClientEnvironment(name, env);
7542
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
7543
+ const diskPaths = credentialDiskSources(name, env);
7544
+ if (options.apiKey !== undefined) {
7545
+ const explicitKey = options.apiKey.trim();
7546
+ if (!explicitKey) {
7547
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
7548
+ }
7549
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
7550
+ return sealCredential({
7551
+ apiKey: explicitKey,
7552
+ tier: "argument",
7553
+ source: "explicit apiKey argument",
7554
+ deliberate: true,
7555
+ diskCandidates: diskPaths,
7556
+ warning: null
7557
+ });
7558
+ }
7559
+ const overrideKeyName = credentialOverrideEnvKey(name);
7560
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
7561
+ if (overrideRaw !== undefined) {
7562
+ const override = overrideRaw.trim();
7563
+ if (!override) {
7564
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
7565
+ }
7566
+ assertUsableCredential(name, overrideKeyName, override);
7567
+ return sealCredential({
7568
+ apiKey: override,
7569
+ tier: "override",
7570
+ source: overrideKeyName,
7571
+ deliberate: true,
7572
+ diskCandidates: diskPaths,
7573
+ warning: null
7574
+ });
7575
+ }
7576
+ const pointerKeyName = credentialPointerEnvKey(name);
7577
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
7578
+ if (pointerRaw !== undefined) {
7579
+ const pointer = pointerRaw.trim();
7580
+ if (!pointer) {
7581
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
7582
+ }
7583
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
7584
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
7585
+ }
7586
+ return sealCredential({
7587
+ apiKey: "",
7588
+ pointerVaultKey: pointer,
7589
+ tier: "pointer",
7590
+ source: pointerKeyName,
7591
+ deliberate: true,
7592
+ diskCandidates: diskPaths,
7593
+ warning: null
7594
+ });
7595
+ }
7596
+ if (options.profile !== undefined && !options.profile.trim()) {
7597
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
7598
+ }
7599
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
7600
+ if (profileRaw !== undefined && !profileRaw.trim()) {
7601
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
7602
+ }
7603
+ const profile = options.profile?.trim() || profileRaw?.trim();
7604
+ if (profile) {
7605
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
7606
+ if (!SAFE_PROFILE.test(profile)) {
7607
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
7608
+ }
7609
+ const paths = profileDiskSources(name, env, profile);
7610
+ for (const path of paths) {
7611
+ const value = readCredentialFile(path, apiKeyKeys);
7612
+ if (value) {
7613
+ assertUsableCredential(name, path, value);
7614
+ return sealCredential({
7615
+ apiKey: value,
7616
+ tier: "profile",
7617
+ source: path,
7618
+ deliberate: true,
7619
+ diskCandidates: paths,
7620
+ warning: null
7621
+ });
7622
+ }
7623
+ }
7624
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
7625
+ }
7626
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
7627
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
7628
+ if (blankEnv) {
7629
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
7630
+ }
7631
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
7632
+ throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
7633
+ }
7634
+ const envHit = firstEnvValue(env, apiKeyKeys);
7635
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
7636
+ if (keychainHit) {
7637
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
7638
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
7639
+ return sealCredential({
7640
+ apiKey: keychainHit.value,
7641
+ tier: "keychain",
7642
+ source: keychainHit.source,
7643
+ deliberate: false,
7644
+ diskCandidates: diskPaths,
7645
+ warning
7646
+ });
7647
+ }
7648
+ const diskSourceList = credentialDiskSourceList(name, env, null);
7649
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
7650
+ if (diskHits.length > 0) {
7651
+ const winner = diskHits[0];
7652
+ assertUsableCredential(name, winner.src.path, winner.value);
7653
+ const divergentSources = [
7654
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
7655
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
7656
+ ];
7657
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
7658
+ return sealCredential({
7659
+ apiKey: winner.value,
7660
+ tier: winner.src.tier,
7661
+ source: winner.src.path,
7662
+ deliberate: false,
7663
+ diskCandidates: diskPaths,
7664
+ warning
7665
+ });
7666
+ }
7667
+ if (envHit) {
7668
+ assertUsableCredential(name, envHit.key, envHit.value);
7669
+ return sealCredential({
7670
+ apiKey: envHit.value,
7671
+ tier: "env",
7672
+ source: envHit.key,
7673
+ deliberate: false,
7674
+ diskCandidates: diskPaths,
7675
+ warning: null
7676
+ });
7677
+ }
7678
+ return null;
7679
+ }
7680
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
7681
+ var requireSecretsSdk = createRequire(import.meta.url);
7682
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
7683
+ var DEFAULT_AUTHORITY_SOURCE = "default";
7684
+ function defaultFleetGatewayBaseUrl(name) {
7685
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
7686
+ }
7687
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
7688
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7689
+ function isValidDnsDomain(value) {
7690
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
7691
+ return false;
7692
+ }
7693
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
7694
+ }
7695
+ function validateAppSlug(name) {
7696
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
7697
+ throw new Error("App name must be one lowercase DNS label.");
7698
+ }
7699
+ return name;
7700
+ }
7701
+ function rawAuthority(value) {
7702
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
7703
+ if (!match)
7704
+ throw new Error("API URL must be absolute.");
7705
+ const afterScheme = value.slice(match[0].length);
7706
+ const boundary = afterScheme.search(/[/?#]/);
7707
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
7708
+ if (!authority)
7709
+ throw new Error("API URL must include a hostname.");
7710
+ return authority;
7711
+ }
7712
+ function assertCanonicalPort(port) {
7713
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
7714
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7715
+ }
7716
+ const numericPort = Number(port);
7717
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
7718
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7719
+ }
7720
+ }
7721
+ function canonicalAuthorityHostname(authority) {
7722
+ let rawHostname;
7723
+ if (authority.startsWith("[")) {
7724
+ const closingBracket = authority.indexOf("]");
7725
+ if (closingBracket === -1) {
7726
+ throw new Error("API URL authority must contain a canonical hostname.");
7727
+ }
7728
+ rawHostname = authority.slice(0, closingBracket + 1);
7729
+ const portSuffix = authority.slice(closingBracket + 1);
7730
+ if (portSuffix) {
7731
+ if (!portSuffix.startsWith(":")) {
7732
+ throw new Error("API URL authority must contain a canonical hostname and port.");
7733
+ }
7734
+ assertCanonicalPort(portSuffix.slice(1));
7735
+ }
7736
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
7737
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
7738
+ }
7739
+ } else {
7740
+ const firstColon = authority.indexOf(":");
7741
+ const lastColon = authority.lastIndexOf(":");
7742
+ if (firstColon !== lastColon) {
7743
+ throw new Error("IPv6 API URL authorities must use brackets.");
7744
+ }
7745
+ if (lastColon !== -1) {
7746
+ const port = authority.slice(lastColon + 1);
7747
+ assertCanonicalPort(port);
7748
+ rawHostname = authority.slice(0, lastColon);
7749
+ } else {
7750
+ rawHostname = authority;
7751
+ }
7752
+ const ipVersion = isIP(rawHostname);
7753
+ const numericAddressParts = rawHostname.split(".");
7754
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
7755
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
7756
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
7757
+ }
7758
+ }
7759
+ return rawHostname.toLowerCase();
7760
+ }
7761
+ function isDeliberateLoopbackHttpAuthority(authority) {
7762
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
7763
+ }
7764
+ function toV1BaseUrl(apiUrl) {
7765
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
7766
+ throw new Error("API URL must not contain ASCII control characters.");
7767
+ }
7768
+ const input = apiUrl.trim();
7769
+ const authority = rawAuthority(input);
7770
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
7771
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
7772
+ }
7773
+ const canonicalHostname = canonicalAuthorityHostname(authority);
7774
+ const url = new URL(input);
7775
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7776
+ throw new Error("API URL must use http or https.");
7777
+ }
7778
+ if (url.username || url.password) {
7779
+ throw new Error("API URL must not include credentials.");
7780
+ }
7781
+ if (!url.hostname || url.hostname.endsWith(".")) {
7782
+ throw new Error("API URL must include a canonical hostname.");
7783
+ }
7784
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
7785
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
7786
+ }
7787
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
7788
+ throw new Error("API URL must not use IDN or punycode hostnames.");
7789
+ }
7790
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
7791
+ throw new Error("API URL may use http only for an exact loopback authority.");
7792
+ }
7793
+ if (url.search || url.hash) {
7794
+ throw new Error("API URL must not include a query string or fragment.");
7795
+ }
7796
+ let path = url.pathname.replace(/\/+$/, "");
7797
+ if (path.endsWith("/v1"))
7798
+ path = path.slice(0, -"/v1".length);
7799
+ url.pathname = `${path}/v1`;
7800
+ return url.toString().replace(/\/+$/, "");
7801
+ }
7802
+ class ClientTransportConfigurationError extends Error {
7803
+ appName;
7804
+ sources;
7805
+ constructor(appName, message, sources = []) {
7806
+ super(message);
7807
+ this.name = "ClientTransportConfigurationError";
7808
+ this.appName = appName;
7809
+ this.sources = Object.freeze([...sources]);
7810
+ }
7811
+ }
7812
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
7813
+ env = snapshotClientEnvironment(name, env);
7814
+ const keys = clientTransportEnvKeys(name);
7815
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
7816
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
7817
+ if (blankUrl) {
7818
+ throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
7819
+ }
7820
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
7821
+ if (controlledUrl) {
7822
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
7823
+ }
7824
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
7825
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
7826
+ throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
7827
+ }
7828
+ const envUrlHit = usableUrlEntries[0] ?? null;
7829
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
7830
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
7831
+ if (diskConfigUrlHit?.unusable) {
7832
+ throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
7833
+ }
7834
+ const urlCandidates = [
7835
+ ...envUrlHit ? [envUrlHit] : [],
7836
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
7837
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
7838
+ ];
7839
+ const configuredUrl = urlCandidates[0] ?? null;
7840
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
7841
+ if (configuredUrl && divergentUrls.length > 0) {
7842
+ throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
7843
+ }
7844
+ const warnings = [];
7845
+ if (configuredUrl && !envUrlHit) {
7846
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
7847
+ }
7848
+ const credential = resolveCredential(name, env, options.credentials);
7849
+ if (!credential) {
7850
+ const diskHint = credentialDiskSourcesForMessage(name, env);
7851
+ const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
7852
+ warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
7853
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
7854
+ }
7855
+ if (credential.warning)
7856
+ warnings.push(credential.warning);
7857
+ let urlHit;
7858
+ if (configuredUrl) {
7859
+ urlHit = configuredUrl;
7860
+ } else {
7861
+ try {
7862
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
7863
+ } catch (error) {
7864
+ const message = error instanceof Error ? error.message : String(error);
7865
+ throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
7866
+ }
7867
+ }
7868
+ const apiUrlSource = urlHit.key;
7869
+ let baseUrl;
7870
+ try {
7871
+ baseUrl = toV1BaseUrl(urlHit.value);
7872
+ } catch (error) {
7873
+ const message = error instanceof Error ? error.message : String(error);
7874
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
7875
+ }
7876
+ return {
7877
+ resolution: {
7878
+ transport: "http",
7879
+ transportSource: urlHit.key,
7880
+ baseUrl,
7881
+ apiUrlSource,
7882
+ apiKeyPresent: true,
7883
+ apiKeySource: credential.source,
7884
+ apiKeyTier: credential.tier,
7885
+ misconfigured: false,
7886
+ warning: warnings.length > 0 ? warnings.join(" ") : null
7887
+ },
7888
+ credential
7889
+ };
7890
+ }
7891
+ function resolveClientTransport(name, env = process.env, options = {}) {
7892
+ return resolveClientTransportSnapshot(name, env, options).resolution;
7893
+ }
7894
+ function credentialDiskSourcesForMessage(name, env) {
7895
+ const paths = credentialDiskSources(name, env);
7896
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
7897
+ }
7898
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7899
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
7900
+ "host",
7901
+ ":authority",
7902
+ "forwarded",
7903
+ "x-forwarded-host",
7904
+ "x-original-host"
7905
+ ]);
7906
+
7907
+ // src/lib/transport.ts
7908
+ init_config();
7909
+
7910
+ // src/lib/local-opt-in.ts
7911
+ var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
7912
+ function isHooksLocalOptIn(env = process.env) {
7913
+ return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
7914
+ }
7915
+ function hooksAuthorityEnvKeys() {
7916
+ const keys = clientTransportEnvKeys("hooks");
7917
+ return [
7918
+ ...keys.apiUrlKeys,
7919
+ ...keys.apiKeyKeys,
7920
+ credentialOverrideEnvKey("hooks"),
7921
+ credentialPointerEnvKey("hooks"),
7922
+ CREDENTIAL_PROFILE_ENV_KEY
7923
+ ];
7924
+ }
7925
+ function hasHooksEnvAuthorityIntent(env = process.env) {
7926
+ return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
7927
+ }
7928
+ function selectsHooksLocalStore(env = process.env) {
7929
+ return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
7930
+ }
7931
+ function hooksResolverEnv(env) {
7932
+ const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
7933
+ if (blanks.length === 0)
7934
+ return env;
7935
+ const next = { ...env };
7936
+ for (const key of blanks)
7937
+ delete next[key];
7938
+ return next;
7939
+ }
7940
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7941
+ function isAmbientHooksEnv(env) {
7942
+ if (typeof process !== "undefined" && env === process.env)
7943
+ return true;
7944
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
7945
+ }
7946
+ function hooksResolverInputs(env, credentials = {}) {
7947
+ const normalised = hooksResolverEnv(env);
7948
+ if (normalised === env)
7949
+ return { env: normalised, credentials };
7950
+ const keychain = { ...credentials.keychain };
7951
+ if (keychain.enabled === undefined && keychain.run === undefined) {
7952
+ keychain.enabled = isAmbientHooksEnv(env);
7953
+ }
7954
+ return { env: normalised, credentials: { ...credentials, keychain } };
7955
+ }
7956
+
7957
+ // src/lib/transport.ts
7958
+ var localNoticePrinted = false;
7959
+ function announceLocal(notice, reason) {
7960
+ if (localNoticePrinted)
7961
+ return;
7962
+ localNoticePrinted = true;
7963
+ const line = `hooks: LOCAL mode \u2014 ${reason}; using the bundled registry and the local store at ` + `${getHooksDataDir()}, not a remote registry. Set HASNA_HOOKS_API_KEY (or the Keychain item ` + `hasna.credentials.hooks.api-key / ~/.hasna/hooks/config/credentials) to go remote.`;
7964
+ if (notice)
7965
+ notice(line);
7966
+ else if (typeof process !== "undefined")
7967
+ process.stderr.write(`${line}
7968
+ `);
7969
+ }
7970
+ function hooksRegistryOrigin(v1BaseUrl) {
7971
+ return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
7972
+ }
7973
+ function rethrowHooksAuthorityFailure(error) {
7974
+ const message = error instanceof Error ? error.message : String(error);
7975
+ const name = error instanceof Error ? error.name : "";
7976
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
7977
+ throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
7978
+ }
7979
+ if (/no API key could be resolved/.test(message)) {
7980
+ if (/is not set and no API key could be resolved/.test(message)) {
7981
+ throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved from the Keychain item " + `hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1, alias HOOKS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
7982
+ }
7983
+ throw new Error("REMOTE_API_KEY_MISSING: the remote hooks registry requires HASNA_HOOKS_API_KEY, the Keychain item " + `hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
7984
+ }
7985
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
7986
+ }
7987
+ function resolveHooksTransport(env = process.env, options = {}) {
7988
+ if (selectsHooksLocalStore(env)) {
7989
+ announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
7990
+ return { mode: "local", source: "local-opt-in", authority: null };
7991
+ }
7992
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
7993
+ const tier1 = options.credentials?.apiKey;
7994
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
7995
+ let resolution;
7996
+ let credential;
7997
+ try {
7998
+ credential = resolveCredential("hooks", resolverEnv, credentialOptions);
7999
+ resolution = resolveClientTransport("hooks", resolverEnv, {
8000
+ credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
8001
+ });
8002
+ } catch (error) {
8003
+ rethrowHooksAuthorityFailure(error);
8004
+ }
8005
+ if (!credential) {
8006
+ throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
8007
+ }
8008
+ return {
8009
+ mode: "remote",
8010
+ source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
8011
+ authority: {
8012
+ origin: hooksRegistryOrigin(resolution.baseUrl),
8013
+ apiKey: credential.apiKey,
8014
+ apiUrlSource: resolution.apiUrlSource,
8015
+ apiKeySource: credential.source,
8016
+ apiKeyTier: credential.tier,
8017
+ v1BaseUrl: resolution.baseUrl,
8018
+ warning: resolution.warning
8019
+ }
8020
+ };
8021
+ }
8022
+ function resolveHooksServePublishKey(env = process.env, options = {}) {
8023
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8024
+ const tier1 = options.credentials?.apiKey;
8025
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
8026
+ return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
8027
+ }
8028
+
8029
+ // src/index.ts
7543
8030
  init_store();
7544
8031
  init_manifest();
7545
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
7546
8032
 
7547
8033
  // src/lib/custom-install.ts
7548
8034
  init_config();
7549
8035
  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";
8036
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
8037
+ import { join as join10 } from "path";
7552
8038
  import { tmpdir } from "os";
7553
8039
  function manifestAt(dir) {
7554
- const path = join11(dir, "manifest.json");
7555
- if (!existsSync10(path))
8040
+ const path = join10(dir, "manifest.json");
8041
+ if (!existsSync9(path))
7556
8042
  return;
7557
8043
  try {
7558
- return parseManifest(readFileSync8(path, "utf-8"));
8044
+ return parseManifest(readFileSync7(path, "utf-8"));
7559
8045
  } catch {
7560
8046
  return;
7561
8047
  }
@@ -7574,8 +8060,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7574
8060
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7575
8061
  }
7576
8062
  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")], {
8063
+ const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
8064
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
7579
8065
  stdout: "pipe",
7580
8066
  stderr: "pipe"
7581
8067
  });
@@ -7585,10 +8071,10 @@ async function cloneToTemp(gitUrl) {
7585
8071
  proc.exited
7586
8072
  ]);
7587
8073
  if (exitCode !== 0) {
7588
- rmSync3(tmp, { recursive: true, force: true });
8074
+ rmSync2(tmp, { recursive: true, force: true });
7589
8075
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7590
8076
  }
7591
- return join11(tmp, "repo");
8077
+ return join10(tmp, "repo");
7592
8078
  }
7593
8079
  function isGitUrl(value) {
7594
8080
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7598,7 +8084,7 @@ function isCustomSource(value) {
7598
8084
  return true;
7599
8085
  if (value.startsWith("http://") || value.startsWith("https://"))
7600
8086
  return true;
7601
- return existsSync10(value);
8087
+ return existsSync9(value);
7602
8088
  }
7603
8089
  async function installCustomSource(source) {
7604
8090
  if (isGitUrl(source)) {
@@ -7606,17 +8092,17 @@ async function installCustomSource(source) {
7606
8092
  try {
7607
8093
  return copyManifestDir(repoDir, source, "git");
7608
8094
  } finally {
7609
- rmSync3(repoDir, { recursive: true, force: true });
8095
+ rmSync2(repoDir, { recursive: true, force: true });
7610
8096
  }
7611
8097
  }
7612
8098
  if (source.startsWith("http://") || source.startsWith("https://")) {
7613
8099
  return installManifestUrl(source);
7614
8100
  }
7615
- if (!existsSync10(source)) {
8101
+ if (!existsSync9(source)) {
7616
8102
  throw new Error(`No such local path: ${source}`);
7617
8103
  }
7618
8104
  if (source.endsWith("manifest.json")) {
7619
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8105
+ const manifest = parseManifest(readFileSync7(source, "utf-8"));
7620
8106
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7621
8107
  }
7622
8108
  return copyManifestDir(source, source, "local");
@@ -7647,8 +8133,7 @@ async function installManifestUrl(url) {
7647
8133
  }
7648
8134
  // src/lib/sync.ts
7649
8135
  import { createHash as createHash2 } from "crypto";
7650
- import { readFileSync as readFileSync9 } from "fs";
7651
- init_config();
8136
+ import { readFileSync as readFileSync8 } from "fs";
7652
8137
  init_db();
7653
8138
  init_store();
7654
8139
  init_manifest();
@@ -7656,7 +8141,7 @@ function sha256OfText(text) {
7656
8141
  return createHash2("sha256").update(text).digest("hex");
7657
8142
  }
7658
8143
  function sha256OfPath(path) {
7659
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8144
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
7660
8145
  }
7661
8146
  function collectBundledCatalog() {
7662
8147
  const out = [];
@@ -7672,9 +8157,8 @@ function collectBundledCatalog() {
7672
8157
  }
7673
8158
  return out;
7674
8159
  }
7675
- async function fetchJson(base, path, opts) {
8160
+ async function fetchJson(base, path, apiKey, opts) {
7676
8161
  const headers = { accept: "application/json" };
7677
- const apiKey = resolveApiKey();
7678
8162
  if (apiKey)
7679
8163
  headers["x-api-key"] = apiKey;
7680
8164
  const res = await fetch(`${base}${path}`, {
@@ -7683,7 +8167,7 @@ async function fetchJson(base, path, opts) {
7683
8167
  signal: AbortSignal.timeout(15000)
7684
8168
  });
7685
8169
  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)");
8170
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, " + "or ~/.hasna/hooks/config/credentials (resolved per call, never stored)");
7687
8171
  }
7688
8172
  if (!res.ok) {
7689
8173
  throw new Error(`GET ${path} failed with status ${res.status}`);
@@ -7694,9 +8178,9 @@ async function fetchJson(base, path, opts) {
7694
8178
  }
7695
8179
  return body;
7696
8180
  }
7697
- async function fetchRemoteState(apiUrl) {
7698
- const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
7699
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8181
+ async function fetchRemoteState(apiUrl, apiKey) {
8182
+ const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
8183
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7700
8184
  if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
7701
8185
  throw new Error("remote catalog is malformed: missing hooks array");
7702
8186
  }
@@ -7739,20 +8223,25 @@ function computeDiff(catalog, remoteLock) {
7739
8223
  return diff;
7740
8224
  }
7741
8225
  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) };
8226
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8227
+ credentials: options.credentials
8228
+ });
8229
+ if (transport.mode === "remote" && transport.authority) {
8230
+ const { origin, apiKey } = transport.authority;
8231
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8232
+ return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
7746
8233
  }
7747
8234
  const diff = computeDiff(collectBundledCatalog(), null);
7748
8235
  return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
7749
8236
  }
7750
8237
  async function syncHooks(options = {}) {
7751
- const apiUrl = resolveApiUrl();
7752
- const plan = await planSync();
8238
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8239
+ credentials: options.credentials
8240
+ });
8241
+ const plan = await planSync(options);
7753
8242
  if (options.dryRun)
7754
8243
  return { ...plan, dryRun: true };
7755
- if (!apiUrl) {
8244
+ if (transport.mode !== "remote" || !transport.authority) {
7756
8245
  const db = getDb();
7757
8246
  for (const entry of collectBundledCatalog()) {
7758
8247
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
@@ -7767,19 +8256,20 @@ async function syncHooks(options = {}) {
7767
8256
  }
7768
8257
  return plan;
7769
8258
  }
7770
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
8259
+ const { origin, apiKey } = transport.authority;
8260
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
7771
8261
  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 };
8262
+ const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
8263
+ await commitSyncArtifacts(staged, origin, remoteLock);
8264
+ return { apiUrl: origin, dryRun: false, diff };
7775
8265
  }
7776
- async function stageSyncArtifacts(apiUrl, names, remoteLock) {
8266
+ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
7777
8267
  const staged = [];
7778
8268
  for (const name of names) {
7779
8269
  const entry = remoteLock.hooks[name];
7780
8270
  if (!entry)
7781
8271
  continue;
7782
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
8272
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
7783
8273
  if (!artifact.manifest || typeof artifact.script !== "string") {
7784
8274
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
7785
8275
  }
@@ -7846,8 +8336,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7846
8336
  throw err;
7847
8337
  }
7848
8338
  }
7849
- async function fetchPinnedHook(name, version, apiUrl) {
7850
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8339
+ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
8340
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7851
8341
  const entry = remoteLock.hooks?.[name];
7852
8342
  if (!entry) {
7853
8343
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
@@ -7856,7 +8346,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
7856
8346
  if (!isLatest && !(entry.versions ?? []).includes(version)) {
7857
8347
  throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
7858
8348
  }
7859
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
8349
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
7860
8350
  if (!artifact.manifest || typeof artifact.script !== "string") {
7861
8351
  throw new Error(`artifact for '${name}@${version}' is malformed`);
7862
8352
  }
@@ -7910,9 +8400,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
7910
8400
  // src/serve.ts
7911
8401
  init_manifest();
7912
8402
  init_store();
7913
- init_config();
7914
- import { readFileSync as readFileSync10 } from "fs";
7915
- import { dirname as dirname4, join as join12 } from "path";
8403
+ import { readFileSync as readFileSync9 } from "fs";
8404
+ import { dirname as dirname4, join as join11 } from "path";
7916
8405
  import { fileURLToPath as fileURLToPath3 } from "url";
7917
8406
 
7918
8407
  // src/lib/secure-compare.ts
@@ -8073,7 +8562,7 @@ var openApiDocument = {
8073
8562
  apiKey: {
8074
8563
  type: "http",
8075
8564
  scheme: "bearer",
8076
- description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
8565
+ description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
8077
8566
  }
8078
8567
  }
8079
8568
  }
@@ -8086,8 +8575,8 @@ var SERVE_HOST = "127.0.0.1";
8086
8575
  var SERVE_SERVICE_NAME = "hooks-registry";
8087
8576
  function packageVersion() {
8088
8577
  try {
8089
- const pkgPath = join12(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8090
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
8578
+ const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8579
+ return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
8091
8580
  } catch {
8092
8581
  return "0.0.0";
8093
8582
  }
@@ -8244,12 +8733,11 @@ function resolveServeOptions(options) {
8244
8733
  }
8245
8734
  function startServeServer(options) {
8246
8735
  const { port, host } = resolveServeOptions(options);
8247
- const apiKey = resolveApiKey();
8248
8736
  const server = Bun.serve({
8249
8737
  hostname: host,
8250
8738
  port,
8251
8739
  async fetch(req) {
8252
- return handleServeRequest(req, apiKey);
8740
+ return handleServeRequest(req, resolveHooksServePublishKey());
8253
8741
  }
8254
8742
  });
8255
8743
  console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
@@ -8354,11 +8842,11 @@ async function runHook(name, input, options = {}) {
8354
8842
  throw new Error(`Hook '${name}' not found`);
8355
8843
  const hookScript = getHookPath(name) + "/src/hook.ts";
8356
8844
  const resolvedScript = custom2?.scriptPath;
8357
- const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
8845
+ const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
8358
8846
  if (!script)
8359
8847
  throw new Error(`Hook script not found: ${name}`);
8360
8848
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
8361
- const content = readFileSync11(script);
8849
+ const content = readFileSync10(script);
8362
8850
  const check = checkScriptHash2(name, sha256Of2(content));
8363
8851
  if (!check.ok) {
8364
8852
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
@@ -8449,7 +8937,6 @@ async function runHook(name, input, options = {}) {
8449
8937
  export {
8450
8938
  writeLock,
8451
8939
  writeCustomHook,
8452
- writeConfig,
8453
8940
  verifyScriptHash,
8454
8941
  upsertHookRecord,
8455
8942
  updateProfile,
@@ -8470,11 +8957,11 @@ export {
8470
8957
  resolveTables,
8471
8958
  resolveScriptPath,
8472
8959
  resolveScript,
8960
+ resolveHooksTransport,
8961
+ resolveHooksServePublishKey,
8473
8962
  resolveHookMeta,
8474
8963
  resolveHookDir,
8475
8964
  resolveHook,
8476
- resolveApiUrl,
8477
- resolveApiKey,
8478
8965
  removeProjectHook,
8479
8966
  removePinnedHook,
8480
8967
  removeHookRecord,
@@ -8482,7 +8969,6 @@ export {
8482
8969
  removeHook,
8483
8970
  readLock,
8484
8971
  readCustomManifest,
8485
- readConfig,
8486
8972
  provisionCloudflareResources,
8487
8973
  planSync,
8488
8974
  pinInstalledHook,
@@ -8500,6 +8986,7 @@ export {
8500
8986
  installHook,
8501
8987
  installCustomSource,
8502
8988
  importProfiles,
8989
+ hooksRegistryOrigin,
8503
8990
  hookExists,
8504
8991
  handleServeRequest,
8505
8992
  getSyncMetaAll,
@@ -8524,7 +9011,6 @@ export {
8524
9011
  getHookEvents,
8525
9012
  getHook,
8526
9013
  getCustomHooksDir,
8527
- getConfigPath,
8528
9014
  fetchPinnedHook,
8529
9015
  exportProfiles,
8530
9016
  deleteProfile,