@hasna/hooks 0.6.1 → 0.6.3

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.
package/bin/index.js CHANGED
@@ -4288,10 +4288,18 @@ __export(exports_resolve, {
4288
4288
  resolveScriptPath: () => resolveScriptPath,
4289
4289
  resolveHookMeta: () => resolveHookMeta,
4290
4290
  resolveHookDir: () => resolveHookDir,
4291
- resolveHook: () => resolveHook
4291
+ resolveHook: () => resolveHook,
4292
+ resolveBundledHooksDir: () => resolveBundledHooksDir
4292
4293
  });
4293
- import { join as join4 } from "path";
4294
+ import { dirname as dirname2, join as join4 } from "path";
4294
4295
  import { existsSync as existsSync4 } from "fs";
4296
+ import { fileURLToPath } from "url";
4297
+ function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
4298
+ const sourceLayout = join4(moduleDir, "..", "..", "hooks");
4299
+ if (existsSync4(join4(sourceLayout, "hook-gitguard")))
4300
+ return sourceLayout;
4301
+ return join4(moduleDir, "..", "hooks");
4302
+ }
4295
4303
  function resolveHookDir(name) {
4296
4304
  const custom = readCustomManifest(name);
4297
4305
  if (custom)
@@ -4300,7 +4308,7 @@ function resolveHookDir(name) {
4300
4308
  if (!meta)
4301
4309
  return;
4302
4310
  const short = shortManifestName(name);
4303
- const bundledBase = join4(__dirname, "..", "..", "hooks");
4311
+ const bundledBase = resolveBundledHooksDir();
4304
4312
  const direct = join4(bundledBase, short);
4305
4313
  if (existsSync4(direct))
4306
4314
  return direct;
@@ -4363,7 +4371,6 @@ function resolveHook(name) {
4363
4371
  meta
4364
4372
  };
4365
4373
  }
4366
- var __dirname = "/home/hasna/.hasna/repos/worktrees/hasna-apps/workspace-repos-guard/apps/hooks/src/lib";
4367
4374
  var init_resolve = __esm(() => {
4368
4375
  init_registry();
4369
4376
  init_manifest();
@@ -4371,9 +4378,9 @@ var init_resolve = __esm(() => {
4371
4378
 
4372
4379
  // src/lib/installer.ts
4373
4380
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4374
- import { join as join5, dirname as dirname2 } from "path";
4381
+ import { join as join5, dirname as dirname3 } from "path";
4375
4382
  import { homedir as homedir3 } from "os";
4376
- import { fileURLToPath } from "url";
4383
+ import { fileURLToPath as fileURLToPath2 } from "url";
4377
4384
  function normalizeHookName(name) {
4378
4385
  return name.startsWith("hook-") ? name : `hook-${name}`;
4379
4386
  }
@@ -4438,7 +4445,7 @@ function readSettings(scope = "global", target = "claude") {
4438
4445
  }
4439
4446
  function writeSettings(settings, scope = "global", target = "claude") {
4440
4447
  const path = getSettingsPath(scope, target);
4441
- const dir = dirname2(path);
4448
+ const dir = dirname3(path);
4442
4449
  if (!existsSync5(dir)) {
4443
4450
  mkdirSync3(dir, { recursive: true });
4444
4451
  }
@@ -4535,7 +4542,7 @@ function readCodewithConfig(scope, configPath) {
4535
4542
  }
4536
4543
  function appendCodewithFragment(fragment, scope, configPath) {
4537
4544
  const path = getSettingsPath(scope, "codewith", configPath);
4538
- const dir = dirname2(path);
4545
+ const dir = dirname3(path);
4539
4546
  if (!existsSync5(dir))
4540
4547
  mkdirSync3(dir, { recursive: true });
4541
4548
  const existing = existsSync5(path) ? readFileSync3(path, "utf-8") : "";
@@ -4786,7 +4793,7 @@ var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
4786
4793
  var init_installer = __esm(() => {
4787
4794
  init_registry();
4788
4795
  init_resolve();
4789
- __dirname2 = dirname2(fileURLToPath(import.meta.url));
4796
+ __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4790
4797
  HOOKS_DIR = existsSync5(join5(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join5(__dirname2, "..", "..", "hooks") : join5(__dirname2, "..", "hooks");
4791
4798
  EVENT_MAP = {
4792
4799
  claude: {
@@ -4823,6 +4830,35 @@ var init_installer = __esm(() => {
4823
4830
  getInstalledHooks = getRegisteredHooks;
4824
4831
  });
4825
4832
 
4833
+ // src/lib/registration.ts
4834
+ function safeMatcherTest(pattern, value) {
4835
+ try {
4836
+ return new RegExp(pattern).test(value);
4837
+ } catch {
4838
+ return false;
4839
+ }
4840
+ }
4841
+ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
4842
+ const separator = eventSpec.indexOf(":");
4843
+ const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
4844
+ const specMatcher = separator === -1 ? "" : eventSpec.slice(separator + 1);
4845
+ const matcher = specMatcher || hookMatcher || "";
4846
+ const hooks = settings.hooks?.[eventName];
4847
+ if (!Array.isArray(hooks))
4848
+ return false;
4849
+ return hooks.some((entry) => entry?.hooks?.some((h) => {
4850
+ const match = h?.command?.match(/^hooks run ([\w-]+)/);
4851
+ if (!match || match[1] !== name)
4852
+ return false;
4853
+ if (!matcher)
4854
+ return true;
4855
+ const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
4856
+ if (!entryMatcher)
4857
+ return true;
4858
+ return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
4859
+ }));
4860
+ }
4861
+
4826
4862
  // src/lib/profiles.ts
4827
4863
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2, rmSync, cpSync } from "fs";
4828
4864
  import { join as join6 } from "path";
@@ -5460,18 +5496,49 @@ __export(exports_run, {
5460
5496
  });
5461
5497
  import { randomBytes } from "crypto";
5462
5498
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
5463
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join9 } from "path";
5499
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
5464
5500
  function extensionOf(scriptPath) {
5465
5501
  const base = basename2(scriptPath);
5466
5502
  const dot = base.lastIndexOf(".");
5467
5503
  return dot > 0 ? base.slice(dot + 1) : "ts";
5468
5504
  }
5505
+ function shebangInterpreter(firstLine) {
5506
+ if (!firstLine.startsWith("#!"))
5507
+ return null;
5508
+ const rest = firstLine.slice(2).trim();
5509
+ if (!rest)
5510
+ return null;
5511
+ const parts = rest.split(/\s+/);
5512
+ if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
5513
+ return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
5514
+ }
5515
+ return basename2(parts[0]);
5516
+ }
5517
+ function interpreterFor(name, scriptPath, content) {
5518
+ const ext = extensionOf(scriptPath);
5519
+ const firstLine = content.subarray(0, 512).toString("utf8").split(`
5520
+ `, 1)[0] ?? "";
5521
+ const interp = shebangInterpreter(firstLine);
5522
+ if (interp) {
5523
+ if (interp === "bash" || interp === "sh")
5524
+ return { command: ["/bin/bash"], tempExt: "sh" };
5525
+ if (interp === "node" || interp === "bun")
5526
+ return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5527
+ throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
5528
+ }
5529
+ if (BASH_EXTENSIONS.has(ext))
5530
+ return { command: ["/bin/bash"], tempExt: "sh" };
5531
+ if (BUN_EXTENSIONS.has(ext))
5532
+ return { command: ["bun", "run"], tempExt: ext };
5533
+ throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
5534
+ }
5469
5535
  async function executeVerifiedScript(options) {
5470
- const scriptDir = dirname3(options.scriptPath);
5471
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
5536
+ const scriptDir = dirname4(options.scriptPath);
5537
+ const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5538
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
5472
5539
  try {
5473
5540
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5474
- const proc = Bun.spawn(["bun", "run", isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5541
+ const proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5475
5542
  stdin: new Response(options.stdin),
5476
5543
  stdout: "pipe",
5477
5544
  stderr: "pipe",
@@ -5490,7 +5557,11 @@ async function executeVerifiedScript(options) {
5490
5557
  } catch {}
5491
5558
  }
5492
5559
  }
5493
- var init_run = () => {};
5560
+ var BASH_EXTENSIONS, BUN_EXTENSIONS;
5561
+ var init_run = __esm(() => {
5562
+ BASH_EXTENSIONS = new Set(["sh", "bash"]);
5563
+ BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
5564
+ });
5494
5565
 
5495
5566
  // src/lib/custom-install.ts
5496
5567
  var exports_custom_install = {};
@@ -6036,6 +6107,19 @@ var init_storage = __esm(() => {
6036
6107
  init_pg_migrations();
6037
6108
  });
6038
6109
 
6110
+ // src/lib/secure-compare.ts
6111
+ function secureEqual(a, b) {
6112
+ const enc = new TextEncoder;
6113
+ const ab = enc.encode(a);
6114
+ const bb = enc.encode(b);
6115
+ let diff = ab.length ^ bb.length;
6116
+ const n = Math.max(ab.length, bb.length);
6117
+ for (let i = 0;i < n; i++) {
6118
+ diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
6119
+ }
6120
+ return diff === 0;
6121
+ }
6122
+
6039
6123
  // src/serve.ts
6040
6124
  var exports_serve = {};
6041
6125
  __export(exports_serve, {
@@ -6097,8 +6181,8 @@ function authorized(req, apiKey) {
6097
6181
  return false;
6098
6182
  const header = req.headers.get("authorization") ?? "";
6099
6183
  if (header.startsWith("Bearer "))
6100
- return header.slice("Bearer ".length) === apiKey;
6101
- return req.headers.get("x-api-key") === apiKey;
6184
+ return secureEqual(header.slice("Bearer ".length), apiKey);
6185
+ return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
6102
6186
  }
6103
6187
  function json(body, status = 200, headers = {}) {
6104
6188
  return new Response(JSON.stringify(body), {
@@ -6200,10 +6284,17 @@ function collectBundledCatalog() {
6200
6284
  return out;
6201
6285
  }
6202
6286
  async function fetchJson(base, path) {
6287
+ const headers = { accept: "application/json" };
6288
+ const apiKey = resolveApiKey();
6289
+ if (apiKey)
6290
+ headers["x-api-key"] = apiKey;
6203
6291
  const res = await fetch(`${base}${path}`, {
6204
- headers: { accept: "application/json" },
6292
+ headers,
6205
6293
  signal: AbortSignal.timeout(15000)
6206
6294
  });
6295
+ if (res.status === 401) {
6296
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
6297
+ }
6207
6298
  if (!res.ok) {
6208
6299
  throw new Error(`GET ${path} failed with status ${res.status}`);
6209
6300
  }
@@ -6418,8 +6509,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6418
6509
  import { z as z2 } from "zod";
6419
6510
  import { createServer } from "http";
6420
6511
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6421
- import { join as join11, dirname as dirname4 } from "path";
6422
- import { fileURLToPath as fileURLToPath2 } from "url";
6512
+ import { join as join11, dirname as dirname5 } from "path";
6513
+ import { fileURLToPath as fileURLToPath3 } from "url";
6423
6514
  async function runVerifiedHook(name, scriptPath, stdin, timeoutMs) {
6424
6515
  const content = readFileSync9(scriptPath);
6425
6516
  const check = checkScriptHash(name, sha256Of(content));
@@ -6594,13 +6685,9 @@ function createHooksServer() {
6594
6685
  if (meta && settingsExist) {
6595
6686
  try {
6596
6687
  const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
6597
- const eventHooks = settings.hooks?.[meta.event] || [];
6598
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
6599
- const match = h.command?.match(/^hooks run ([\w-]+)/);
6600
- return match && match[1] === name;
6601
- }));
6602
- if (!found) {
6603
- issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
6688
+ if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
6689
+ const eventName = meta.event.split(":")[0];
6690
+ issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
6604
6691
  hookHealthy = false;
6605
6692
  }
6606
6693
  } catch {}
@@ -6957,8 +7044,8 @@ function createHooksServer() {
6957
7044
  disabled.push(name);
6958
7045
  settings.hooks.__disabled = disabled;
6959
7046
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
6960
- const { dirname: dirname5 } = await import("path");
6961
- mkdirSync8(dirname5(settingsPath), { recursive: true });
7047
+ const { dirname: dirname6 } = await import("path");
7048
+ mkdirSync8(dirname6(settingsPath), { recursive: true });
6962
7049
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
6963
7050
  `);
6964
7051
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -7290,7 +7377,7 @@ var init_server = __esm(() => {
7290
7377
  init_resolve();
7291
7378
  init_store();
7292
7379
  init_storage();
7293
- __dirname3 = dirname4(fileURLToPath2(import.meta.url));
7380
+ __dirname3 = dirname5(fileURLToPath3(import.meta.url));
7294
7381
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
7295
7382
  try {
7296
7383
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
@@ -8567,8 +8654,8 @@ var {
8567
8654
  // src/cli/index.tsx
8568
8655
  import chalk2 from "chalk";
8569
8656
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8570
- import { join as join12, dirname as dirname5 } from "path";
8571
- import { fileURLToPath as fileURLToPath3 } from "url";
8657
+ import { join as join12, dirname as dirname6 } from "path";
8658
+ import { fileURLToPath as fileURLToPath4 } from "url";
8572
8659
 
8573
8660
  // src/cli/components/App.tsx
8574
8661
  import { useState as useState7 } from "react";
@@ -9746,7 +9833,7 @@ init_profiles();
9746
9833
  init_manifest();
9747
9834
  init_resolve();
9748
9835
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
9749
- var __dirname4 = dirname5(fileURLToPath3(import.meta.url));
9836
+ var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
9750
9837
  var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
9751
9838
  var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
9752
9839
  var program2 = new Command;
@@ -10312,13 +10399,9 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
10312
10399
  if (meta && settingsExist) {
10313
10400
  try {
10314
10401
  const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
10315
- const eventHooks = settings.hooks?.[meta.event] || [];
10316
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
10317
- const match = h.command?.match(/^hooks run ([\w-]+)/);
10318
- return match && match[1] === name;
10319
- }));
10320
- if (!found) {
10321
- issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
10402
+ if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
10403
+ const eventName = meta.event.split(":")[0];
10404
+ issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
10322
10405
  hookHealthy = false;
10323
10406
  }
10324
10407
  } catch {}
package/bin/serve.js CHANGED
@@ -5051,9 +5051,15 @@ function retrustHook(name, scriptPath, version, source) {
5051
5051
  }
5052
5052
 
5053
5053
  // src/lib/resolve.ts
5054
- import { join as join5 } from "path";
5054
+ import { dirname as dirname2, join as join5 } from "path";
5055
5055
  import { existsSync as existsSync5 } from "fs";
5056
- var __dirname = "/home/hasna/.hasna/repos/worktrees/hasna-apps/workspace-repos-guard/apps/hooks/src/lib";
5056
+ import { fileURLToPath } from "url";
5057
+ function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
5058
+ const sourceLayout = join5(moduleDir, "..", "..", "hooks");
5059
+ if (existsSync5(join5(sourceLayout, "hook-gitguard")))
5060
+ return sourceLayout;
5061
+ return join5(moduleDir, "..", "hooks");
5062
+ }
5057
5063
  function resolveHookDir(name) {
5058
5064
  const custom2 = readCustomManifest(name);
5059
5065
  if (custom2)
@@ -5062,7 +5068,7 @@ function resolveHookDir(name) {
5062
5068
  if (!meta)
5063
5069
  return;
5064
5070
  const short = shortManifestName(name);
5065
- const bundledBase = join5(__dirname, "..", "..", "hooks");
5071
+ const bundledBase = resolveBundledHooksDir();
5066
5072
  const direct = join5(bundledBase, short);
5067
5073
  if (existsSync5(direct))
5068
5074
  return direct;
@@ -5126,6 +5132,19 @@ function resolveHook(name) {
5126
5132
  };
5127
5133
  }
5128
5134
 
5135
+ // src/lib/secure-compare.ts
5136
+ function secureEqual(a, b) {
5137
+ const enc = new TextEncoder;
5138
+ const ab = enc.encode(a);
5139
+ const bb = enc.encode(b);
5140
+ let diff = ab.length ^ bb.length;
5141
+ const n = Math.max(ab.length, bb.length);
5142
+ for (let i = 0;i < n; i++) {
5143
+ diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
5144
+ }
5145
+ return diff === 0;
5146
+ }
5147
+
5129
5148
  // src/serve.ts
5130
5149
  var DEFAULT_SERVE_PORT = 39428;
5131
5150
  var SERVE_HOST = "127.0.0.1";
@@ -5182,8 +5201,8 @@ function authorized(req, apiKey) {
5182
5201
  return false;
5183
5202
  const header = req.headers.get("authorization") ?? "";
5184
5203
  if (header.startsWith("Bearer "))
5185
- return header.slice("Bearer ".length) === apiKey;
5186
- return req.headers.get("x-api-key") === apiKey;
5204
+ return secureEqual(header.slice("Bearer ".length), apiKey);
5205
+ return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
5187
5206
  }
5188
5207
  function json(body, status = 200, headers = {}) {
5189
5208
  return new Response(JSON.stringify(body), {
package/dist/index.js CHANGED
@@ -4849,18 +4849,49 @@ __export(exports_run, {
4849
4849
  });
4850
4850
  import { randomBytes } from "crypto";
4851
4851
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
4852
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join9 } from "path";
4852
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
4853
4853
  function extensionOf(scriptPath) {
4854
4854
  const base = basename2(scriptPath);
4855
4855
  const dot = base.lastIndexOf(".");
4856
4856
  return dot > 0 ? base.slice(dot + 1) : "ts";
4857
4857
  }
4858
+ function shebangInterpreter(firstLine) {
4859
+ if (!firstLine.startsWith("#!"))
4860
+ return null;
4861
+ const rest = firstLine.slice(2).trim();
4862
+ if (!rest)
4863
+ return null;
4864
+ const parts = rest.split(/\s+/);
4865
+ if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
4866
+ return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
4867
+ }
4868
+ return basename2(parts[0]);
4869
+ }
4870
+ function interpreterFor(name, scriptPath, content) {
4871
+ const ext = extensionOf(scriptPath);
4872
+ const firstLine = content.subarray(0, 512).toString("utf8").split(`
4873
+ `, 1)[0] ?? "";
4874
+ const interp = shebangInterpreter(firstLine);
4875
+ if (interp) {
4876
+ if (interp === "bash" || interp === "sh")
4877
+ return { command: ["/bin/bash"], tempExt: "sh" };
4878
+ if (interp === "node" || interp === "bun")
4879
+ return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
4880
+ throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
4881
+ }
4882
+ if (BASH_EXTENSIONS.has(ext))
4883
+ return { command: ["/bin/bash"], tempExt: "sh" };
4884
+ if (BUN_EXTENSIONS.has(ext))
4885
+ return { command: ["bun", "run"], tempExt: ext };
4886
+ throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
4887
+ }
4858
4888
  async function executeVerifiedScript(options) {
4859
- const scriptDir = dirname3(options.scriptPath);
4860
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
4889
+ const scriptDir = dirname4(options.scriptPath);
4890
+ const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
4891
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
4861
4892
  try {
4862
4893
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
4863
- const proc = Bun.spawn(["bun", "run", isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
4894
+ const proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
4864
4895
  stdin: new Response(options.stdin),
4865
4896
  stdout: "pipe",
4866
4897
  stderr: "pipe",
@@ -4879,7 +4910,11 @@ async function executeVerifiedScript(options) {
4879
4910
  } catch {}
4880
4911
  }
4881
4912
  }
4882
- var init_run = () => {};
4913
+ var BASH_EXTENSIONS, BUN_EXTENSIONS;
4914
+ var init_run = __esm(() => {
4915
+ BASH_EXTENSIONS = new Set(["sh", "bash"]);
4916
+ BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
4917
+ });
4883
4918
 
4884
4919
  // src/db/storage-sync.ts
4885
4920
  init_db();
@@ -5813,15 +5848,21 @@ function getHook(name) {
5813
5848
  }
5814
5849
  // src/lib/installer.ts
5815
5850
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5816
- import { join as join6, dirname as dirname2 } from "path";
5851
+ import { join as join6, dirname as dirname3 } from "path";
5817
5852
  import { homedir as homedir4 } from "os";
5818
- import { fileURLToPath } from "url";
5853
+ import { fileURLToPath as fileURLToPath2 } from "url";
5819
5854
 
5820
5855
  // src/lib/resolve.ts
5821
- import { join as join5 } from "path";
5856
+ import { dirname as dirname2, join as join5 } from "path";
5822
5857
  import { existsSync as existsSync5 } from "fs";
5858
+ import { fileURLToPath } from "url";
5823
5859
  init_manifest();
5824
- var __dirname = "/home/hasna/.hasna/repos/worktrees/hasna-apps/workspace-repos-guard/apps/hooks/src/lib";
5860
+ function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
5861
+ const sourceLayout = join5(moduleDir, "..", "..", "hooks");
5862
+ if (existsSync5(join5(sourceLayout, "hook-gitguard")))
5863
+ return sourceLayout;
5864
+ return join5(moduleDir, "..", "hooks");
5865
+ }
5825
5866
  function resolveHookDir(name) {
5826
5867
  const custom2 = readCustomManifest(name);
5827
5868
  if (custom2)
@@ -5830,7 +5871,7 @@ function resolveHookDir(name) {
5830
5871
  if (!meta)
5831
5872
  return;
5832
5873
  const short = shortManifestName(name);
5833
- const bundledBase = join5(__dirname, "..", "..", "hooks");
5874
+ const bundledBase = resolveBundledHooksDir();
5834
5875
  const direct = join5(bundledBase, short);
5835
5876
  if (existsSync5(direct))
5836
5877
  return direct;
@@ -5895,7 +5936,7 @@ function resolveHook(name) {
5895
5936
  }
5896
5937
 
5897
5938
  // src/lib/installer.ts
5898
- var __dirname2 = dirname2(fileURLToPath(import.meta.url));
5939
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5899
5940
  var HOOKS_DIR = existsSync6(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
5900
5941
  function normalizeHookName(name) {
5901
5942
  return name.startsWith("hook-") ? name : `hook-${name}`;
@@ -5993,7 +6034,7 @@ function readSettings(scope = "global", target = "claude") {
5993
6034
  }
5994
6035
  function writeSettings(settings, scope = "global", target = "claude") {
5995
6036
  const path = getSettingsPath(scope, target);
5996
- const dir = dirname2(path);
6037
+ const dir = dirname3(path);
5997
6038
  if (!existsSync6(dir)) {
5998
6039
  mkdirSync4(dir, { recursive: true });
5999
6040
  }
@@ -6090,7 +6131,7 @@ function readCodewithConfig(scope, configPath) {
6090
6131
  }
6091
6132
  function appendCodewithFragment(fragment, scope, configPath) {
6092
6133
  const path = getSettingsPath(scope, "codewith", configPath);
6093
- const dir = dirname2(path);
6134
+ const dir = dirname3(path);
6094
6135
  if (!existsSync6(dir))
6095
6136
  mkdirSync4(dir, { recursive: true });
6096
6137
  const existing = existsSync6(path) ? readFileSync4(path, "utf-8") : "";
@@ -6476,10 +6517,17 @@ function collectBundledCatalog() {
6476
6517
  return out;
6477
6518
  }
6478
6519
  async function fetchJson(base, path) {
6520
+ const headers = { accept: "application/json" };
6521
+ const apiKey = resolveApiKey();
6522
+ if (apiKey)
6523
+ headers["x-api-key"] = apiKey;
6479
6524
  const res = await fetch(`${base}${path}`, {
6480
- headers: { accept: "application/json" },
6525
+ headers,
6481
6526
  signal: AbortSignal.timeout(15000)
6482
6527
  });
6528
+ if (res.status === 401) {
6529
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
6530
+ }
6483
6531
  if (!res.ok) {
6484
6532
  throw new Error(`GET ${path} failed with status ${res.status}`);
6485
6533
  }
@@ -6594,6 +6642,21 @@ async function syncHooks(options = {}) {
6594
6642
  init_manifest();
6595
6643
  init_store();
6596
6644
  init_config();
6645
+
6646
+ // src/lib/secure-compare.ts
6647
+ function secureEqual(a, b) {
6648
+ const enc = new TextEncoder;
6649
+ const ab = enc.encode(a);
6650
+ const bb = enc.encode(b);
6651
+ let diff = ab.length ^ bb.length;
6652
+ const n = Math.max(ab.length, bb.length);
6653
+ for (let i = 0;i < n; i++) {
6654
+ diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
6655
+ }
6656
+ return diff === 0;
6657
+ }
6658
+
6659
+ // src/serve.ts
6597
6660
  var DEFAULT_SERVE_PORT = 39428;
6598
6661
  var SERVE_HOST = "127.0.0.1";
6599
6662
  var SERVE_SERVICE_NAME = "hooks-registry";
@@ -6649,8 +6712,8 @@ function authorized(req, apiKey) {
6649
6712
  return false;
6650
6713
  const header = req.headers.get("authorization") ?? "";
6651
6714
  if (header.startsWith("Bearer "))
6652
- return header.slice("Bearer ".length) === apiKey;
6653
- return req.headers.get("x-api-key") === apiKey;
6715
+ return secureEqual(header.slice("Bearer ".length), apiKey);
6716
+ return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
6654
6717
  }
6655
6718
  function json(body, status = 200, headers = {}) {
6656
6719
  return new Response(JSON.stringify(body), {
@@ -13,6 +13,14 @@ export interface ResolvedHook {
13
13
  scriptPath: string;
14
14
  meta: HookMeta;
15
15
  }
16
+ /**
17
+ * Locate the bundled hooks directory at runtime from the executing module's
18
+ * own location. A bare `__dirname` is baked to the builder's path at bundle
19
+ * time, so bundled hooks resolve only on the machine that built the package
20
+ * (0.6.0/0.6.1 regression); `import.meta.url` is preserved at runtime by the
21
+ * bun build target, so resolution follows the installed package instead.
22
+ */
23
+ export declare function resolveBundledHooksDir(moduleDir?: string): string;
16
24
  export declare function resolveHookDir(name: string): string | undefined;
17
25
  export declare function resolveScriptPath(name: string): string | undefined;
18
26
  export declare function resolveHookMeta(name: string): HookMeta | undefined;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Constant-time string comparison for API-key checks.
3
+ *
4
+ * The loop always runs over the longer input and the result accumulates
5
+ * XORs, so the timing does not reveal where or whether the values differ.
6
+ * Available on both the bun/node and workerd runtimes.
7
+ */
8
+ export declare function secureEqual(a: string, b: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {