@hasna/hooks 0.6.1 → 0.6.2

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,14 +5496,14 @@ __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
  }
5469
5505
  async function executeVerifiedScript(options) {
5470
- const scriptDir = dirname3(options.scriptPath);
5506
+ const scriptDir = dirname4(options.scriptPath);
5471
5507
  const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
5472
5508
  try {
5473
5509
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
@@ -6036,6 +6072,19 @@ var init_storage = __esm(() => {
6036
6072
  init_pg_migrations();
6037
6073
  });
6038
6074
 
6075
+ // src/lib/secure-compare.ts
6076
+ function secureEqual(a, b) {
6077
+ const enc = new TextEncoder;
6078
+ const ab = enc.encode(a);
6079
+ const bb = enc.encode(b);
6080
+ let diff = ab.length ^ bb.length;
6081
+ const n = Math.max(ab.length, bb.length);
6082
+ for (let i = 0;i < n; i++) {
6083
+ diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
6084
+ }
6085
+ return diff === 0;
6086
+ }
6087
+
6039
6088
  // src/serve.ts
6040
6089
  var exports_serve = {};
6041
6090
  __export(exports_serve, {
@@ -6097,8 +6146,8 @@ function authorized(req, apiKey) {
6097
6146
  return false;
6098
6147
  const header = req.headers.get("authorization") ?? "";
6099
6148
  if (header.startsWith("Bearer "))
6100
- return header.slice("Bearer ".length) === apiKey;
6101
- return req.headers.get("x-api-key") === apiKey;
6149
+ return secureEqual(header.slice("Bearer ".length), apiKey);
6150
+ return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
6102
6151
  }
6103
6152
  function json(body, status = 200, headers = {}) {
6104
6153
  return new Response(JSON.stringify(body), {
@@ -6200,10 +6249,17 @@ function collectBundledCatalog() {
6200
6249
  return out;
6201
6250
  }
6202
6251
  async function fetchJson(base, path) {
6252
+ const headers = { accept: "application/json" };
6253
+ const apiKey = resolveApiKey();
6254
+ if (apiKey)
6255
+ headers["x-api-key"] = apiKey;
6203
6256
  const res = await fetch(`${base}${path}`, {
6204
- headers: { accept: "application/json" },
6257
+ headers,
6205
6258
  signal: AbortSignal.timeout(15000)
6206
6259
  });
6260
+ if (res.status === 401) {
6261
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
6262
+ }
6207
6263
  if (!res.ok) {
6208
6264
  throw new Error(`GET ${path} failed with status ${res.status}`);
6209
6265
  }
@@ -6418,8 +6474,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6418
6474
  import { z as z2 } from "zod";
6419
6475
  import { createServer } from "http";
6420
6476
  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";
6477
+ import { join as join11, dirname as dirname5 } from "path";
6478
+ import { fileURLToPath as fileURLToPath3 } from "url";
6423
6479
  async function runVerifiedHook(name, scriptPath, stdin, timeoutMs) {
6424
6480
  const content = readFileSync9(scriptPath);
6425
6481
  const check = checkScriptHash(name, sha256Of(content));
@@ -6594,13 +6650,9 @@ function createHooksServer() {
6594
6650
  if (meta && settingsExist) {
6595
6651
  try {
6596
6652
  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" });
6653
+ if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
6654
+ const eventName = meta.event.split(":")[0];
6655
+ issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
6604
6656
  hookHealthy = false;
6605
6657
  }
6606
6658
  } catch {}
@@ -6957,8 +7009,8 @@ function createHooksServer() {
6957
7009
  disabled.push(name);
6958
7010
  settings.hooks.__disabled = disabled;
6959
7011
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
6960
- const { dirname: dirname5 } = await import("path");
6961
- mkdirSync8(dirname5(settingsPath), { recursive: true });
7012
+ const { dirname: dirname6 } = await import("path");
7013
+ mkdirSync8(dirname6(settingsPath), { recursive: true });
6962
7014
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
6963
7015
  `);
6964
7016
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -7290,7 +7342,7 @@ var init_server = __esm(() => {
7290
7342
  init_resolve();
7291
7343
  init_store();
7292
7344
  init_storage();
7293
- __dirname3 = dirname4(fileURLToPath2(import.meta.url));
7345
+ __dirname3 = dirname5(fileURLToPath3(import.meta.url));
7294
7346
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
7295
7347
  try {
7296
7348
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
@@ -8567,8 +8619,8 @@ var {
8567
8619
  // src/cli/index.tsx
8568
8620
  import chalk2 from "chalk";
8569
8621
  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";
8622
+ import { join as join12, dirname as dirname6 } from "path";
8623
+ import { fileURLToPath as fileURLToPath4 } from "url";
8572
8624
 
8573
8625
  // src/cli/components/App.tsx
8574
8626
  import { useState as useState7 } from "react";
@@ -9746,7 +9798,7 @@ init_profiles();
9746
9798
  init_manifest();
9747
9799
  init_resolve();
9748
9800
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
9749
- var __dirname4 = dirname5(fileURLToPath3(import.meta.url));
9801
+ var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
9750
9802
  var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
9751
9803
  var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
9752
9804
  var program2 = new Command;
@@ -10312,13 +10364,9 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
10312
10364
  if (meta && settingsExist) {
10313
10365
  try {
10314
10366
  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" });
10367
+ if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
10368
+ const eventName = meta.event.split(":")[0];
10369
+ issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
10322
10370
  hookHealthy = false;
10323
10371
  }
10324
10372
  } 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,14 +4849,14 @@ __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
4858
  async function executeVerifiedScript(options) {
4859
- const scriptDir = dirname3(options.scriptPath);
4859
+ const scriptDir = dirname4(options.scriptPath);
4860
4860
  const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
4861
4861
  try {
4862
4862
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
@@ -5813,15 +5813,21 @@ function getHook(name) {
5813
5813
  }
5814
5814
  // src/lib/installer.ts
5815
5815
  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";
5816
+ import { join as join6, dirname as dirname3 } from "path";
5817
5817
  import { homedir as homedir4 } from "os";
5818
- import { fileURLToPath } from "url";
5818
+ import { fileURLToPath as fileURLToPath2 } from "url";
5819
5819
 
5820
5820
  // src/lib/resolve.ts
5821
- import { join as join5 } from "path";
5821
+ import { dirname as dirname2, join as join5 } from "path";
5822
5822
  import { existsSync as existsSync5 } from "fs";
5823
+ import { fileURLToPath } from "url";
5823
5824
  init_manifest();
5824
- var __dirname = "/home/hasna/.hasna/repos/worktrees/hasna-apps/workspace-repos-guard/apps/hooks/src/lib";
5825
+ function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
5826
+ const sourceLayout = join5(moduleDir, "..", "..", "hooks");
5827
+ if (existsSync5(join5(sourceLayout, "hook-gitguard")))
5828
+ return sourceLayout;
5829
+ return join5(moduleDir, "..", "hooks");
5830
+ }
5825
5831
  function resolveHookDir(name) {
5826
5832
  const custom2 = readCustomManifest(name);
5827
5833
  if (custom2)
@@ -5830,7 +5836,7 @@ function resolveHookDir(name) {
5830
5836
  if (!meta)
5831
5837
  return;
5832
5838
  const short = shortManifestName(name);
5833
- const bundledBase = join5(__dirname, "..", "..", "hooks");
5839
+ const bundledBase = resolveBundledHooksDir();
5834
5840
  const direct = join5(bundledBase, short);
5835
5841
  if (existsSync5(direct))
5836
5842
  return direct;
@@ -5895,7 +5901,7 @@ function resolveHook(name) {
5895
5901
  }
5896
5902
 
5897
5903
  // src/lib/installer.ts
5898
- var __dirname2 = dirname2(fileURLToPath(import.meta.url));
5904
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5899
5905
  var HOOKS_DIR = existsSync6(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
5900
5906
  function normalizeHookName(name) {
5901
5907
  return name.startsWith("hook-") ? name : `hook-${name}`;
@@ -5993,7 +5999,7 @@ function readSettings(scope = "global", target = "claude") {
5993
5999
  }
5994
6000
  function writeSettings(settings, scope = "global", target = "claude") {
5995
6001
  const path = getSettingsPath(scope, target);
5996
- const dir = dirname2(path);
6002
+ const dir = dirname3(path);
5997
6003
  if (!existsSync6(dir)) {
5998
6004
  mkdirSync4(dir, { recursive: true });
5999
6005
  }
@@ -6090,7 +6096,7 @@ function readCodewithConfig(scope, configPath) {
6090
6096
  }
6091
6097
  function appendCodewithFragment(fragment, scope, configPath) {
6092
6098
  const path = getSettingsPath(scope, "codewith", configPath);
6093
- const dir = dirname2(path);
6099
+ const dir = dirname3(path);
6094
6100
  if (!existsSync6(dir))
6095
6101
  mkdirSync4(dir, { recursive: true });
6096
6102
  const existing = existsSync6(path) ? readFileSync4(path, "utf-8") : "";
@@ -6476,10 +6482,17 @@ function collectBundledCatalog() {
6476
6482
  return out;
6477
6483
  }
6478
6484
  async function fetchJson(base, path) {
6485
+ const headers = { accept: "application/json" };
6486
+ const apiKey = resolveApiKey();
6487
+ if (apiKey)
6488
+ headers["x-api-key"] = apiKey;
6479
6489
  const res = await fetch(`${base}${path}`, {
6480
- headers: { accept: "application/json" },
6490
+ headers,
6481
6491
  signal: AbortSignal.timeout(15000)
6482
6492
  });
6493
+ if (res.status === 401) {
6494
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
6495
+ }
6483
6496
  if (!res.ok) {
6484
6497
  throw new Error(`GET ${path} failed with status ${res.status}`);
6485
6498
  }
@@ -6594,6 +6607,21 @@ async function syncHooks(options = {}) {
6594
6607
  init_manifest();
6595
6608
  init_store();
6596
6609
  init_config();
6610
+
6611
+ // src/lib/secure-compare.ts
6612
+ function secureEqual(a, b) {
6613
+ const enc = new TextEncoder;
6614
+ const ab = enc.encode(a);
6615
+ const bb = enc.encode(b);
6616
+ let diff = ab.length ^ bb.length;
6617
+ const n = Math.max(ab.length, bb.length);
6618
+ for (let i = 0;i < n; i++) {
6619
+ diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
6620
+ }
6621
+ return diff === 0;
6622
+ }
6623
+
6624
+ // src/serve.ts
6597
6625
  var DEFAULT_SERVE_PORT = 39428;
6598
6626
  var SERVE_HOST = "127.0.0.1";
6599
6627
  var SERVE_SERVICE_NAME = "hooks-registry";
@@ -6649,8 +6677,8 @@ function authorized(req, apiKey) {
6649
6677
  return false;
6650
6678
  const header = req.headers.get("authorization") ?? "";
6651
6679
  if (header.startsWith("Bearer "))
6652
- return header.slice("Bearer ".length) === apiKey;
6653
- return req.headers.get("x-api-key") === apiKey;
6680
+ return secureEqual(header.slice("Bearer ".length), apiKey);
6681
+ return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
6654
6682
  }
6655
6683
  function json(body, status = 200, headers = {}) {
6656
6684
  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.2",
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": {