@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 +122 -39
- package/bin/serve.js +24 -5
- package/dist/index.js +79 -16
- package/dist/lib/resolve.d.ts +8 -0
- package/dist/lib/secure-compare.d.ts +8 -0
- package/package.json +1 -1
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
5471
|
-
const
|
|
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([
|
|
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
|
|
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)
|
|
6101
|
-
return req.headers.get("x-api-key")
|
|
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
|
|
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
|
|
6422
|
-
import { fileURLToPath as
|
|
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
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
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:
|
|
6961
|
-
mkdirSync8(
|
|
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 =
|
|
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
|
|
8571
|
-
import { fileURLToPath as
|
|
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 =
|
|
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
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
5186
|
-
return req.headers.get("x-api-key")
|
|
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
|
|
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 =
|
|
4860
|
-
const
|
|
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([
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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)
|
|
6653
|
-
return req.headers.get("x-api-key")
|
|
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), {
|
package/dist/lib/resolve.d.ts
CHANGED
|
@@ -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