@hasna/hooks 0.6.5 → 0.6.8
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/README.md +1 -1
- package/bin/index.js +697 -181
- package/bin/serve.js +111 -28
- package/dist/config.d.ts +8 -1
- package/dist/db/remote-storage.d.ts +22 -0
- package/dist/index.js +506 -113
- package/dist/lib/hook-env.d.ts +98 -0
- package/dist/lib/manifest.d.ts +35 -2
- package/dist/lib/redact.d.ts +40 -0
- package/dist/lib/semver.d.ts +46 -0
- package/dist/lib/store.d.ts +16 -0
- package/dist/lib/sync.d.ts +55 -4
- package/dist/serve.d.ts +2 -1
- package/dist/storage.js +104 -7
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -399,6 +399,60 @@ var init_db = __esm(() => {
|
|
|
399
399
|
init_legacy_import();
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
// src/lib/redact.ts
|
|
403
|
+
function redactText(value) {
|
|
404
|
+
let out = value;
|
|
405
|
+
for (const pattern of SECRET_SHAPES) {
|
|
406
|
+
out = out.replace(pattern, REDACTED);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
function redactValue(value) {
|
|
411
|
+
if (typeof value === "string")
|
|
412
|
+
return redactText(value);
|
|
413
|
+
if (Array.isArray(value))
|
|
414
|
+
return value.map(redactValue);
|
|
415
|
+
if (value !== null && typeof value === "object") {
|
|
416
|
+
const out = {};
|
|
417
|
+
for (const [key, child] of Object.entries(value)) {
|
|
418
|
+
out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue(child);
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
function redactEventPayload(text) {
|
|
425
|
+
if (text === null || text === undefined)
|
|
426
|
+
return null;
|
|
427
|
+
try {
|
|
428
|
+
const parsed = JSON.parse(text);
|
|
429
|
+
return redactText(JSON.stringify(redactValue(parsed)));
|
|
430
|
+
} catch {
|
|
431
|
+
return redactText(text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
|
|
435
|
+
var init_redact = __esm(() => {
|
|
436
|
+
SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
|
|
437
|
+
SECRET_SHAPES = [
|
|
438
|
+
/\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
|
|
439
|
+
/\bsk-[A-Za-z0-9]{16,}/g,
|
|
440
|
+
/\btpe_[A-Za-z0-9]{8,}/g,
|
|
441
|
+
/\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
442
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
443
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
444
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
445
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
446
|
+
/ASIA[0-9A-Z]{16}/g,
|
|
447
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
448
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
449
|
+
/-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
450
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
451
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
452
|
+
/\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
|
|
453
|
+
];
|
|
454
|
+
});
|
|
455
|
+
|
|
402
456
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
403
457
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
404
458
|
const t = typeof data;
|
|
@@ -4414,24 +4468,30 @@ function resolveApiUrl() {
|
|
|
4414
4468
|
const cfg = readConfig();
|
|
4415
4469
|
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4416
4470
|
}
|
|
4417
|
-
function resolveApiKey(
|
|
4418
|
-
if (flagKey && flagKey.length > 0)
|
|
4419
|
-
return flagKey;
|
|
4471
|
+
function resolveApiKey() {
|
|
4420
4472
|
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4421
4473
|
}
|
|
4422
4474
|
var init_config = () => {};
|
|
4423
4475
|
|
|
4476
|
+
// src/lib/semver.ts
|
|
4477
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4478
|
+
var init_semver = __esm(() => {
|
|
4479
|
+
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4424
4482
|
// src/lib/manifest.ts
|
|
4425
4483
|
var exports_manifest = {};
|
|
4426
4484
|
__export(exports_manifest, {
|
|
4427
4485
|
writeCustomHook: () => writeCustomHook,
|
|
4428
4486
|
shortManifestName: () => shortManifestName,
|
|
4487
|
+
scriptRelFor: () => scriptRelFor,
|
|
4429
4488
|
resolveScript: () => resolveScript,
|
|
4430
4489
|
readCustomManifest: () => readCustomManifest,
|
|
4431
4490
|
parseManifest: () => parseManifest,
|
|
4432
4491
|
normalizeManifestName: () => normalizeManifestName,
|
|
4433
4492
|
manifestSchema: () => manifestSchema,
|
|
4434
4493
|
listCustomHooks: () => listCustomHooks,
|
|
4494
|
+
isInlineScript: () => isInlineScript,
|
|
4435
4495
|
customHookDir: () => customHookDir,
|
|
4436
4496
|
assertContained: () => assertContained,
|
|
4437
4497
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
@@ -4484,16 +4544,25 @@ function assertContained(target, root, raw) {
|
|
|
4484
4544
|
assertContainedReal(root, lexical, raw);
|
|
4485
4545
|
return lexical;
|
|
4486
4546
|
}
|
|
4487
|
-
function
|
|
4488
|
-
|
|
4489
|
-
`)
|
|
4547
|
+
function isInlineScript(manifest) {
|
|
4548
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4549
|
+
`);
|
|
4550
|
+
}
|
|
4551
|
+
function scriptRelFor(manifest) {
|
|
4552
|
+
if (isInlineScript(manifest)) {
|
|
4490
4553
|
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4491
|
-
return
|
|
4554
|
+
return `script${ext}`;
|
|
4492
4555
|
}
|
|
4493
4556
|
const rel = normalize(manifest.script);
|
|
4494
4557
|
if (isAbsolute(rel)) {
|
|
4495
4558
|
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4496
4559
|
}
|
|
4560
|
+
return rel;
|
|
4561
|
+
}
|
|
4562
|
+
function resolveScript(manifest, manifestDir) {
|
|
4563
|
+
if (isInlineScript(manifest))
|
|
4564
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4565
|
+
const rel = scriptRelFor(manifest);
|
|
4497
4566
|
const resolved = assertContained(join4(manifestDir, rel), manifestDir, manifest.script);
|
|
4498
4567
|
if (!existsSync4(resolved)) {
|
|
4499
4568
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
@@ -4511,8 +4580,8 @@ function readCustomManifest(name) {
|
|
|
4511
4580
|
try {
|
|
4512
4581
|
const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
|
|
4513
4582
|
const script = resolveScript(manifest, dir);
|
|
4514
|
-
|
|
4515
|
-
|
|
4583
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4584
|
+
return { manifest, scriptPath: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4516
4585
|
} catch (err) {
|
|
4517
4586
|
if (err instanceof ScriptContainmentError)
|
|
4518
4587
|
throw err;
|
|
@@ -4559,15 +4628,18 @@ var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
|
4559
4628
|
var init_manifest = __esm(() => {
|
|
4560
4629
|
init_zod();
|
|
4561
4630
|
init_config();
|
|
4631
|
+
init_semver();
|
|
4562
4632
|
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4563
4633
|
manifestSchema = exports_external.object({
|
|
4564
4634
|
name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4565
|
-
version: exports_external.string().regex(
|
|
4635
|
+
version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4566
4636
|
description: exports_external.string().optional(),
|
|
4567
4637
|
events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
|
|
4568
4638
|
script: exports_external.string().min(1, "script must name a relative path or inline content"),
|
|
4639
|
+
script_kind: exports_external.enum(["inline", "file"]).optional(),
|
|
4569
4640
|
args: exports_external.array(exports_external.string()).optional(),
|
|
4570
|
-
timeout_ms: exports_external.number().int().positive().optional()
|
|
4641
|
+
timeout_ms: exports_external.number().int().positive().optional(),
|
|
4642
|
+
env: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
4571
4643
|
});
|
|
4572
4644
|
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4573
4645
|
constructor(path, hookDir, raw) {
|
|
@@ -4595,11 +4667,13 @@ __export(exports_store, {
|
|
|
4595
4667
|
listHookRecords: () => listHookRecords,
|
|
4596
4668
|
getPinnedHook: () => getPinnedHook,
|
|
4597
4669
|
getHookRecord: () => getHookRecord,
|
|
4598
|
-
checkScriptHash: () => checkScriptHash
|
|
4670
|
+
checkScriptHash: () => checkScriptHash,
|
|
4671
|
+
LockFileError: () => LockFileError
|
|
4599
4672
|
});
|
|
4600
4673
|
import { createHash } from "crypto";
|
|
4601
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4674
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4602
4675
|
import { readFile } from "fs/promises";
|
|
4676
|
+
import { randomBytes } from "crypto";
|
|
4603
4677
|
function sha256Of(input) {
|
|
4604
4678
|
return createHash("sha256").update(input).digest("hex");
|
|
4605
4679
|
}
|
|
@@ -4642,16 +4716,39 @@ function removeHookRecord(db, name) {
|
|
|
4642
4716
|
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
4643
4717
|
return res.changes > 0;
|
|
4644
4718
|
}
|
|
4719
|
+
function assertLockShape(parsed, path) {
|
|
4720
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4721
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4722
|
+
}
|
|
4723
|
+
const hooks = parsed.hooks;
|
|
4724
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
4725
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4726
|
+
}
|
|
4727
|
+
for (const [name, entry] of Object.entries(hooks)) {
|
|
4728
|
+
const e = entry;
|
|
4729
|
+
if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
|
|
4730
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4645
4734
|
function readLock() {
|
|
4735
|
+
const path = getLockPath();
|
|
4736
|
+
if (!existsSync6(path))
|
|
4737
|
+
return { hooks: {} };
|
|
4738
|
+
let raw;
|
|
4646
4739
|
try {
|
|
4647
|
-
|
|
4648
|
-
if (!existsSync6(path))
|
|
4649
|
-
return { hooks: {} };
|
|
4650
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4651
|
-
return { hooks: parsed.hooks ?? {} };
|
|
4740
|
+
raw = readFileSync4(path, "utf-8");
|
|
4652
4741
|
} catch {
|
|
4653
|
-
|
|
4742
|
+
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4743
|
+
}
|
|
4744
|
+
let parsed;
|
|
4745
|
+
try {
|
|
4746
|
+
parsed = JSON.parse(raw);
|
|
4747
|
+
} catch {
|
|
4748
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4654
4749
|
}
|
|
4750
|
+
assertLockShape(parsed, path);
|
|
4751
|
+
return parsed;
|
|
4655
4752
|
}
|
|
4656
4753
|
function writeLock(lock) {
|
|
4657
4754
|
const path = getLockPath();
|
|
@@ -4660,8 +4757,10 @@ function writeLock(lock) {
|
|
|
4660
4757
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4661
4758
|
sorted[name] = lock.hooks[name];
|
|
4662
4759
|
}
|
|
4663
|
-
|
|
4760
|
+
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
4761
|
+
writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4664
4762
|
`, "utf-8");
|
|
4763
|
+
renameSync(tmp, path);
|
|
4665
4764
|
return path;
|
|
4666
4765
|
}
|
|
4667
4766
|
function setPinnedHook(name, entry) {
|
|
@@ -4747,9 +4846,16 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
4747
4846
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
4748
4847
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
4749
4848
|
}
|
|
4849
|
+
var LockFileError;
|
|
4750
4850
|
var init_store = __esm(() => {
|
|
4751
4851
|
init_db();
|
|
4752
4852
|
init_config();
|
|
4853
|
+
LockFileError = class LockFileError extends Error {
|
|
4854
|
+
constructor(message) {
|
|
4855
|
+
super(message);
|
|
4856
|
+
this.name = "LockFileError";
|
|
4857
|
+
}
|
|
4858
|
+
};
|
|
4753
4859
|
});
|
|
4754
4860
|
|
|
4755
4861
|
// src/lib/profiles.ts
|
|
@@ -4895,15 +5001,169 @@ var init_profiles = __esm(() => {
|
|
|
4895
5001
|
PROFILES_DIR = resolveProfilesDir();
|
|
4896
5002
|
});
|
|
4897
5003
|
|
|
5004
|
+
// src/lib/hook-env.ts
|
|
5005
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
5006
|
+
import { existsSync as existsSync10, statSync } from "fs";
|
|
5007
|
+
function isDeniedEnvName(name) {
|
|
5008
|
+
const upper = name.toUpperCase();
|
|
5009
|
+
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
5010
|
+
return true;
|
|
5011
|
+
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
5012
|
+
return true;
|
|
5013
|
+
return DENY_CONTAINS.some((token) => upper.includes(token));
|
|
5014
|
+
}
|
|
5015
|
+
function isInterpreterInjectionEnvName(name) {
|
|
5016
|
+
if (INTERPRETER_INJECTION.has(name))
|
|
5017
|
+
return true;
|
|
5018
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
5019
|
+
}
|
|
5020
|
+
function isBashFunctionEnvName(name) {
|
|
5021
|
+
return name.startsWith("BASH_FUNC_");
|
|
5022
|
+
}
|
|
5023
|
+
function isUnsafePathEntry(entry, home) {
|
|
5024
|
+
if (entry === "" || !isAbsolute2(entry))
|
|
5025
|
+
return true;
|
|
5026
|
+
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
5027
|
+
return true;
|
|
5028
|
+
if (entry === "/tmp" || entry.startsWith("/tmp/"))
|
|
5029
|
+
return true;
|
|
5030
|
+
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
5031
|
+
return true;
|
|
5032
|
+
try {
|
|
5033
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
5034
|
+
return true;
|
|
5035
|
+
} catch {}
|
|
5036
|
+
return false;
|
|
5037
|
+
}
|
|
5038
|
+
function sanitizeHookPath(pathValue, home) {
|
|
5039
|
+
const baseline = PATH_BASELINE.slice();
|
|
5040
|
+
if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
|
|
5041
|
+
baseline.push("/opt/homebrew/bin");
|
|
5042
|
+
const bunDir = dirname4(process.execPath);
|
|
5043
|
+
const seen = new Set;
|
|
5044
|
+
const out = [];
|
|
5045
|
+
for (const dir of baseline) {
|
|
5046
|
+
if (seen.has(dir))
|
|
5047
|
+
continue;
|
|
5048
|
+
seen.add(dir);
|
|
5049
|
+
out.push(dir);
|
|
5050
|
+
}
|
|
5051
|
+
if (!seen.has(bunDir)) {
|
|
5052
|
+
seen.add(bunDir);
|
|
5053
|
+
out.push(bunDir);
|
|
5054
|
+
}
|
|
5055
|
+
for (const entry of pathValue.split(":")) {
|
|
5056
|
+
if (seen.has(entry))
|
|
5057
|
+
continue;
|
|
5058
|
+
if (isUnsafePathEntry(entry, home))
|
|
5059
|
+
continue;
|
|
5060
|
+
seen.add(entry);
|
|
5061
|
+
out.push(entry);
|
|
5062
|
+
}
|
|
5063
|
+
return out.join(":");
|
|
5064
|
+
}
|
|
5065
|
+
function buildHookEnv(source, extra) {
|
|
5066
|
+
const out = {};
|
|
5067
|
+
for (const name of ALLOWLIST) {
|
|
5068
|
+
if (name === "PATH")
|
|
5069
|
+
continue;
|
|
5070
|
+
const value = source[name] ?? extra?.[name];
|
|
5071
|
+
if (value !== undefined && value !== null)
|
|
5072
|
+
out[name] = value;
|
|
5073
|
+
}
|
|
5074
|
+
const pathOverride = extra?.PATH;
|
|
5075
|
+
if (pathOverride !== undefined && pathOverride !== null) {
|
|
5076
|
+
out.PATH = pathOverride;
|
|
5077
|
+
} else {
|
|
5078
|
+
const parentPath = source.PATH;
|
|
5079
|
+
if (parentPath !== undefined && parentPath !== null) {
|
|
5080
|
+
out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
for (const [from, to] of CONFIG_PROJECTIONS) {
|
|
5084
|
+
const value = source[from] ?? extra?.[from];
|
|
5085
|
+
if (value !== undefined && value !== null)
|
|
5086
|
+
out[to] = value;
|
|
5087
|
+
}
|
|
5088
|
+
const merged = { ...source, ...extra ?? {} };
|
|
5089
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
5090
|
+
if (out[name] !== undefined)
|
|
5091
|
+
continue;
|
|
5092
|
+
if (isDeniedEnvName(name))
|
|
5093
|
+
continue;
|
|
5094
|
+
if (isInterpreterInjectionEnvName(name))
|
|
5095
|
+
continue;
|
|
5096
|
+
if (isBashFunctionEnvName(name))
|
|
5097
|
+
continue;
|
|
5098
|
+
if (value !== undefined && value !== null)
|
|
5099
|
+
out[name] = value;
|
|
5100
|
+
}
|
|
5101
|
+
return out;
|
|
5102
|
+
}
|
|
5103
|
+
var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
|
|
5104
|
+
var init_hook_env = __esm(() => {
|
|
5105
|
+
ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
|
|
5106
|
+
INTERPRETER_INJECTION = new Set([
|
|
5107
|
+
"BASH_ENV",
|
|
5108
|
+
"ENV",
|
|
5109
|
+
"BASHOPTS",
|
|
5110
|
+
"SHELLOPTS",
|
|
5111
|
+
"NODE_OPTIONS",
|
|
5112
|
+
"NODE_PATH",
|
|
5113
|
+
"PYTHONSTARTUP",
|
|
5114
|
+
"PYTHONINSPECT",
|
|
5115
|
+
"PYTHONPATH",
|
|
5116
|
+
"PYTHONHOME",
|
|
5117
|
+
"LD_PRELOAD",
|
|
5118
|
+
"LD_LIBRARY_PATH",
|
|
5119
|
+
"GCONV_PATH",
|
|
5120
|
+
"LOCPATH",
|
|
5121
|
+
"PERL5OPT",
|
|
5122
|
+
"RUBYOPT",
|
|
5123
|
+
"NODE_EXTRA_CA_CERTS",
|
|
5124
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
5125
|
+
"SSL_CERT_FILE",
|
|
5126
|
+
"SSL_CERT_DIR",
|
|
5127
|
+
"CURL_CA_BUNDLE",
|
|
5128
|
+
"GIT_SSL_CAINFO",
|
|
5129
|
+
"AWS_CA_BUNDLE"
|
|
5130
|
+
]);
|
|
5131
|
+
INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
|
|
5132
|
+
DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
|
|
5133
|
+
DENY_PREFIXES = [
|
|
5134
|
+
"HASNA_",
|
|
5135
|
+
"AWS_",
|
|
5136
|
+
"AZURE_",
|
|
5137
|
+
"GCP_",
|
|
5138
|
+
"VAULT_",
|
|
5139
|
+
"GOOGLE_",
|
|
5140
|
+
"OPENAI_",
|
|
5141
|
+
"ANTHROPIC_",
|
|
5142
|
+
"POSTGRES_",
|
|
5143
|
+
"MYSQL_",
|
|
5144
|
+
"REDIS_",
|
|
5145
|
+
"MONGO_",
|
|
5146
|
+
"MEMENTOS_"
|
|
5147
|
+
];
|
|
5148
|
+
DENY_CONTAINS = ["DATABASE_URL"];
|
|
5149
|
+
PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
|
|
5150
|
+
CONFIG_PROJECTIONS = [
|
|
5151
|
+
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
5152
|
+
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
5153
|
+
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
5154
|
+
["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
|
|
5155
|
+
];
|
|
5156
|
+
});
|
|
5157
|
+
|
|
4898
5158
|
// src/lib/run.ts
|
|
4899
5159
|
var exports_run = {};
|
|
4900
5160
|
__export(exports_run, {
|
|
4901
5161
|
executeVerifiedScript: () => executeVerifiedScript,
|
|
4902
5162
|
HookTimeoutError: () => HookTimeoutError
|
|
4903
5163
|
});
|
|
4904
|
-
import { randomBytes } from "crypto";
|
|
5164
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4905
5165
|
import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4906
|
-
import { basename as basename2, dirname as
|
|
5166
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
|
|
4907
5167
|
function extensionOf(scriptPath) {
|
|
4908
5168
|
const base = basename2(scriptPath);
|
|
4909
5169
|
const dot = base.lastIndexOf(".");
|
|
@@ -4930,13 +5190,13 @@ function interpreterFor(name, scriptPath, content) {
|
|
|
4930
5190
|
if (interp === "bash" || interp === "sh")
|
|
4931
5191
|
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
4932
5192
|
if (interp === "node" || interp === "bun")
|
|
4933
|
-
return { command: [
|
|
5193
|
+
return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
|
|
4934
5194
|
throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
|
|
4935
5195
|
}
|
|
4936
5196
|
if (BASH_EXTENSIONS.has(ext))
|
|
4937
5197
|
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
4938
5198
|
if (BUN_EXTENSIONS.has(ext))
|
|
4939
|
-
return { command: [
|
|
5199
|
+
return { command: [process.execPath, "run"], tempExt: ext };
|
|
4940
5200
|
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)`);
|
|
4941
5201
|
}
|
|
4942
5202
|
async function readPipeWithDeadline(stream, deadlineMs) {
|
|
@@ -4972,9 +5232,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
4972
5232
|
return chunks.join("");
|
|
4973
5233
|
}
|
|
4974
5234
|
async function executeVerifiedScript(options) {
|
|
4975
|
-
const scriptDir =
|
|
5235
|
+
const scriptDir = dirname5(options.scriptPath);
|
|
4976
5236
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
4977
|
-
const tempPath = join9(scriptDir, `.hooks-verified-${
|
|
5237
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
4978
5238
|
let proc = null;
|
|
4979
5239
|
let timedOut = false;
|
|
4980
5240
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -4983,11 +5243,11 @@ async function executeVerifiedScript(options) {
|
|
|
4983
5243
|
}, options.timeout) : null;
|
|
4984
5244
|
try {
|
|
4985
5245
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
4986
|
-
proc = Bun.spawn([...interpreter.command,
|
|
5246
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
4987
5247
|
stdin: new Response(options.stdin),
|
|
4988
5248
|
stdout: "pipe",
|
|
4989
5249
|
stderr: "pipe",
|
|
4990
|
-
env:
|
|
5250
|
+
env: buildHookEnv(process.env, options.env),
|
|
4991
5251
|
detached: true
|
|
4992
5252
|
});
|
|
4993
5253
|
const exitCode = await proc.exited;
|
|
@@ -5045,6 +5305,7 @@ function killGroup(proc, options) {
|
|
|
5045
5305
|
}
|
|
5046
5306
|
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5047
5307
|
var init_run = __esm(() => {
|
|
5308
|
+
init_hook_env();
|
|
5048
5309
|
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5049
5310
|
timeoutMs;
|
|
5050
5311
|
constructor(timeoutMs) {
|
|
@@ -5073,6 +5334,9 @@ function writeHookEvent(event) {
|
|
|
5073
5334
|
const db = getDb();
|
|
5074
5335
|
const id = nanoid2();
|
|
5075
5336
|
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5337
|
+
const toolInput = redactEventPayload(event.tool_input);
|
|
5338
|
+
const error = redactEventPayload(event.error);
|
|
5339
|
+
const metadata = redactEventPayload(event.metadata);
|
|
5076
5340
|
db.run(`INSERT INTO hook_events
|
|
5077
5341
|
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5078
5342
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
@@ -5082,12 +5346,12 @@ function writeHookEvent(event) {
|
|
|
5082
5346
|
event.hook_name,
|
|
5083
5347
|
event.event_type,
|
|
5084
5348
|
event.tool_name ?? null,
|
|
5085
|
-
|
|
5349
|
+
toolInput ? toolInput.slice(0, 500) : null,
|
|
5086
5350
|
event.result ?? null,
|
|
5087
|
-
|
|
5351
|
+
error ?? null,
|
|
5088
5352
|
event.duration_ms ?? null,
|
|
5089
5353
|
event.project_dir ?? null,
|
|
5090
|
-
|
|
5354
|
+
metadata ?? null
|
|
5091
5355
|
]);
|
|
5092
5356
|
} catch (err) {
|
|
5093
5357
|
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
@@ -5137,6 +5401,7 @@ function recordHookRun(record) {
|
|
|
5137
5401
|
var ALLOWED_EVENT_TYPES;
|
|
5138
5402
|
var init_db_writer = __esm(() => {
|
|
5139
5403
|
init_db();
|
|
5404
|
+
init_redact();
|
|
5140
5405
|
ALLOWED_EVENT_TYPES = new Set([
|
|
5141
5406
|
"PreToolUse",
|
|
5142
5407
|
"PostToolUse",
|
|
@@ -5159,7 +5424,7 @@ var PG_MIGRATIONS = [
|
|
|
5159
5424
|
timestamp TEXT NOT NULL,
|
|
5160
5425
|
session_id TEXT NOT NULL,
|
|
5161
5426
|
hook_name TEXT NOT NULL,
|
|
5162
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
5427
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
5163
5428
|
tool_name TEXT,
|
|
5164
5429
|
tool_input TEXT,
|
|
5165
5430
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -5192,7 +5457,10 @@ var PG_MIGRATIONS = [
|
|
|
5192
5457
|
)`,
|
|
5193
5458
|
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
5194
5459
|
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
5195
|
-
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))
|
|
5460
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
|
|
5461
|
+
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
5462
|
+
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
5463
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
|
|
5196
5464
|
];
|
|
5197
5465
|
|
|
5198
5466
|
// src/db/remote-storage.ts
|
|
@@ -5206,7 +5474,34 @@ function normalizeParams(params) {
|
|
|
5206
5474
|
return flat.map((value) => value === undefined ? null : value);
|
|
5207
5475
|
}
|
|
5208
5476
|
function sslConfigFor(connectionString) {
|
|
5209
|
-
|
|
5477
|
+
const wantsSsl = wantsSslFor(connectionString);
|
|
5478
|
+
if (!wantsSsl)
|
|
5479
|
+
return;
|
|
5480
|
+
const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
|
|
5481
|
+
if (insecure && false) {}
|
|
5482
|
+
return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
|
|
5483
|
+
}
|
|
5484
|
+
function wantsSslFor(connectionString) {
|
|
5485
|
+
let raw;
|
|
5486
|
+
try {
|
|
5487
|
+
raw = new URL(connectionString).search.replace(/^\?/, "");
|
|
5488
|
+
} catch {
|
|
5489
|
+
raw = connectionString;
|
|
5490
|
+
}
|
|
5491
|
+
try {
|
|
5492
|
+
raw = decodeURIComponent(raw);
|
|
5493
|
+
} catch {}
|
|
5494
|
+
const paramValue = (name) => {
|
|
5495
|
+
const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
|
|
5496
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
5497
|
+
};
|
|
5498
|
+
const sslmode = paramValue("sslmode");
|
|
5499
|
+
if (sslmode !== undefined)
|
|
5500
|
+
return sslmode !== "disable" && sslmode !== "allow";
|
|
5501
|
+
const ssl = paramValue("ssl");
|
|
5502
|
+
if (ssl !== undefined)
|
|
5503
|
+
return ssl !== "false" && ssl !== "0" && ssl !== "no";
|
|
5504
|
+
return false;
|
|
5210
5505
|
}
|
|
5211
5506
|
|
|
5212
5507
|
class PgAdapterAsync {
|
|
@@ -5228,6 +5523,7 @@ class PgAdapterAsync {
|
|
|
5228
5523
|
}
|
|
5229
5524
|
|
|
5230
5525
|
// src/db/storage-sync.ts
|
|
5526
|
+
init_redact();
|
|
5231
5527
|
var STORAGE_TABLES = [
|
|
5232
5528
|
"hook_events",
|
|
5233
5529
|
"schema_migrations",
|
|
@@ -5404,9 +5700,15 @@ async function pushTable(db, remote, table) {
|
|
|
5404
5700
|
result.rowsRead = rows.length;
|
|
5405
5701
|
if (rows.length === 0)
|
|
5406
5702
|
return result;
|
|
5703
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
5704
|
+
...row,
|
|
5705
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
5706
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
5707
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
5708
|
+
})) : rows;
|
|
5407
5709
|
const remoteColumns = await getRemoteColumns(remote, table);
|
|
5408
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(
|
|
5409
|
-
result.rowsWritten = await upsertPg(remote, table, columns,
|
|
5710
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
|
|
5711
|
+
result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
|
|
5410
5712
|
} catch (error) {
|
|
5411
5713
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
5412
5714
|
}
|
|
@@ -5421,8 +5723,14 @@ async function pullTable(remote, db, table) {
|
|
|
5421
5723
|
result.rowsRead = rows.length;
|
|
5422
5724
|
if (rows.length === 0)
|
|
5423
5725
|
return result;
|
|
5424
|
-
const
|
|
5425
|
-
|
|
5726
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
5727
|
+
...row,
|
|
5728
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
5729
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
5730
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
5731
|
+
})) : rows;
|
|
5732
|
+
const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
|
|
5733
|
+
result.rowsWritten = upsertSqlite(db, table, columns, projected);
|
|
5426
5734
|
} catch (error) {
|
|
5427
5735
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
5428
5736
|
}
|
|
@@ -6236,7 +6544,7 @@ function getGlobalSettingsPathOverride(target) {
|
|
|
6236
6544
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
6237
6545
|
if (target === "codewith" && codewithConfigPath)
|
|
6238
6546
|
return codewithConfigPath;
|
|
6239
|
-
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
6547
|
+
if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
6240
6548
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
6241
6549
|
}
|
|
6242
6550
|
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
@@ -6670,8 +6978,8 @@ function escapeRegExp(value) {
|
|
|
6670
6978
|
function codewithHasHookEntryInText(configText, name) {
|
|
6671
6979
|
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
6672
6980
|
}
|
|
6673
|
-
function codewithHasHookEntry(name) {
|
|
6674
|
-
const path = getSettingsPath(
|
|
6981
|
+
function codewithHasHookEntry(name, scope = "global") {
|
|
6982
|
+
const path = getSettingsPath(scope, "codewith");
|
|
6675
6983
|
if (!existsSync7(path))
|
|
6676
6984
|
return false;
|
|
6677
6985
|
try {
|
|
@@ -6688,7 +6996,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6688
6996
|
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
6689
6997
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
6690
6998
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
6691
|
-
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
6999
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
6692
7000
|
if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
6693
7001
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
6694
7002
|
}
|
|
@@ -6705,7 +7013,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6705
7013
|
}
|
|
6706
7014
|
const registrationsRemaining = [];
|
|
6707
7015
|
if (target === "codewith" || target === "all") {
|
|
6708
|
-
const codewithPath = getSettingsPath(
|
|
7016
|
+
const codewithPath = getSettingsPath(scope, "codewith");
|
|
6709
7017
|
if (existsSync7(codewithPath)) {
|
|
6710
7018
|
const before = readFileSync5(codewithPath, "utf-8");
|
|
6711
7019
|
const after = removeCodewithHookEntry(before, shortName);
|
|
@@ -6751,7 +7059,7 @@ init_profiles();
|
|
|
6751
7059
|
init_config();
|
|
6752
7060
|
init_store();
|
|
6753
7061
|
init_manifest();
|
|
6754
|
-
import { existsSync as
|
|
7062
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
6755
7063
|
|
|
6756
7064
|
// src/lib/custom-install.ts
|
|
6757
7065
|
init_config();
|
|
@@ -6831,19 +7139,20 @@ async function installCustomSource(source) {
|
|
|
6831
7139
|
return copyManifestDir(source, source, "local");
|
|
6832
7140
|
}
|
|
6833
7141
|
async function installManifestUrl(url) {
|
|
6834
|
-
const res = await fetch(url, { redirect: "
|
|
7142
|
+
const res = await fetch(url, { redirect: "error" });
|
|
6835
7143
|
if (!res.ok) {
|
|
6836
7144
|
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6837
7145
|
}
|
|
6838
7146
|
const manifest = parseManifest(await res.text());
|
|
6839
7147
|
let script;
|
|
6840
|
-
|
|
6841
|
-
`)
|
|
7148
|
+
const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
7149
|
+
`);
|
|
7150
|
+
if (inline) {
|
|
6842
7151
|
script = { path: "script.ts", content: manifest.script };
|
|
6843
7152
|
} else {
|
|
6844
7153
|
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6845
7154
|
const scriptUrl = `${base}/${manifest.script}`;
|
|
6846
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "
|
|
7155
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "error" });
|
|
6847
7156
|
if (!scriptRes.ok) {
|
|
6848
7157
|
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6849
7158
|
}
|
|
@@ -6880,7 +7189,7 @@ function collectBundledCatalog() {
|
|
|
6880
7189
|
}
|
|
6881
7190
|
return out;
|
|
6882
7191
|
}
|
|
6883
|
-
async function fetchJson(base, path) {
|
|
7192
|
+
async function fetchJson(base, path, opts) {
|
|
6884
7193
|
const headers = { accept: "application/json" };
|
|
6885
7194
|
const apiKey = resolveApiKey();
|
|
6886
7195
|
if (apiKey)
|
|
@@ -6896,7 +7205,11 @@ async function fetchJson(base, path) {
|
|
|
6896
7205
|
if (!res.ok) {
|
|
6897
7206
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6898
7207
|
}
|
|
6899
|
-
|
|
7208
|
+
const body = await res.json();
|
|
7209
|
+
if (opts?.withHeader) {
|
|
7210
|
+
body[opts.withHeader] = res.headers.get("x-hook-sha256");
|
|
7211
|
+
}
|
|
7212
|
+
return body;
|
|
6900
7213
|
}
|
|
6901
7214
|
async function fetchRemoteState(apiUrl) {
|
|
6902
7215
|
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
@@ -6909,6 +7222,16 @@ async function fetchRemoteState(apiUrl) {
|
|
|
6909
7222
|
}
|
|
6910
7223
|
return { catalog: catalog.hooks, remoteLock };
|
|
6911
7224
|
}
|
|
7225
|
+
function classifyRemotePin(pin, remoteVersion, remoteSha) {
|
|
7226
|
+
const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
|
|
7227
|
+
if (pinnedLocally)
|
|
7228
|
+
return "unchanged";
|
|
7229
|
+
if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
|
|
7230
|
+
return "unchanged";
|
|
7231
|
+
if (pin !== undefined)
|
|
7232
|
+
return "updated";
|
|
7233
|
+
return "added";
|
|
7234
|
+
}
|
|
6912
7235
|
function computeDiff(catalog, remoteLock) {
|
|
6913
7236
|
const local = readLock();
|
|
6914
7237
|
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
@@ -6916,30 +7239,30 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6916
7239
|
if (!entry.name || !entry.version || !entry.sha256)
|
|
6917
7240
|
continue;
|
|
6918
7241
|
const pin = local.hooks[entry.name];
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
diff.
|
|
7242
|
+
if (remoteLock) {
|
|
7243
|
+
const remotePin = remoteLock.hooks[entry.name];
|
|
7244
|
+
if (!remotePin) {
|
|
7245
|
+
diff.skipped.push(entry.name);
|
|
7246
|
+
continue;
|
|
7247
|
+
}
|
|
7248
|
+
if (remotePin.version !== entry.version) {
|
|
7249
|
+
throw new Error(`remote registry is ambiguous for '${entry.name}': lock pins ${remotePin.version} but catalog latest is ${entry.version}. ` + `Fix the registry (or its lock) before syncing \u2014 nothing was changed.`);
|
|
7250
|
+
}
|
|
7251
|
+
diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
|
|
6929
7252
|
} else {
|
|
6930
|
-
diff.
|
|
7253
|
+
diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
|
|
6931
7254
|
}
|
|
6932
7255
|
}
|
|
6933
7256
|
return diff;
|
|
6934
7257
|
}
|
|
6935
|
-
async function planSync() {
|
|
7258
|
+
async function planSync(options = {}) {
|
|
6936
7259
|
const apiUrl = resolveApiUrl();
|
|
6937
7260
|
if (apiUrl) {
|
|
6938
7261
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6939
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
7262
|
+
return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6940
7263
|
}
|
|
6941
7264
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6942
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
7265
|
+
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6943
7266
|
}
|
|
6944
7267
|
async function syncHooks(options = {}) {
|
|
6945
7268
|
const apiUrl = resolveApiUrl();
|
|
@@ -6947,10 +7270,10 @@ async function syncHooks(options = {}) {
|
|
|
6947
7270
|
if (options.dryRun)
|
|
6948
7271
|
return { ...plan, dryRun: true };
|
|
6949
7272
|
if (!apiUrl) {
|
|
6950
|
-
const
|
|
7273
|
+
const db = getDb();
|
|
6951
7274
|
for (const entry of collectBundledCatalog()) {
|
|
6952
7275
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6953
|
-
upsertHookRecord(
|
|
7276
|
+
upsertHookRecord(db, {
|
|
6954
7277
|
name: entry.name,
|
|
6955
7278
|
version: entry.version,
|
|
6956
7279
|
sha256: entry.sha256,
|
|
@@ -6963,13 +7286,17 @@ async function syncHooks(options = {}) {
|
|
|
6963
7286
|
}
|
|
6964
7287
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6965
7288
|
const diff = computeDiff(catalog, remoteLock);
|
|
6966
|
-
const
|
|
6967
|
-
|
|
6968
|
-
|
|
7289
|
+
const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
|
|
7290
|
+
await commitSyncArtifacts(staged, apiUrl, remoteLock);
|
|
7291
|
+
return { apiUrl, dryRun: false, diff };
|
|
7292
|
+
}
|
|
7293
|
+
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
7294
|
+
const staged = [];
|
|
7295
|
+
for (const name of names) {
|
|
6969
7296
|
const entry = remoteLock.hooks[name];
|
|
6970
7297
|
if (!entry)
|
|
6971
7298
|
continue;
|
|
6972
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
7299
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
|
|
6973
7300
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6974
7301
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6975
7302
|
}
|
|
@@ -6977,34 +7304,64 @@ async function syncHooks(options = {}) {
|
|
|
6977
7304
|
if (actualSha !== entry.sha256) {
|
|
6978
7305
|
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6979
7306
|
}
|
|
6980
|
-
artifacts.set(name, artifact);
|
|
6981
|
-
}
|
|
6982
|
-
const db = getDb();
|
|
6983
|
-
for (const name of toFetch) {
|
|
6984
|
-
const artifact = artifacts.get(name);
|
|
6985
|
-
const entry = remoteLock.hooks[name];
|
|
6986
|
-
if (!artifact || !entry)
|
|
6987
|
-
continue;
|
|
6988
7307
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6989
|
-
|
|
6990
|
-
`
|
|
6991
|
-
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6992
|
-
const scriptPath = resolveScriptPath(name);
|
|
6993
|
-
const actual = await sha256File(scriptPath);
|
|
6994
|
-
if (actual !== entry.sha256) {
|
|
6995
|
-
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7308
|
+
if (manifest.version !== entry.version) {
|
|
7309
|
+
throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
|
|
6996
7310
|
}
|
|
6997
|
-
|
|
6998
|
-
|
|
7311
|
+
const scriptRel = scriptRelFor(manifest);
|
|
7312
|
+
staged.push({
|
|
6999
7313
|
name,
|
|
7000
7314
|
version: entry.version,
|
|
7001
7315
|
sha256: entry.sha256,
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7316
|
+
source: entry.source ?? "remote",
|
|
7317
|
+
manifest,
|
|
7318
|
+
scriptContent: artifact.script,
|
|
7319
|
+
scriptRel
|
|
7005
7320
|
});
|
|
7006
7321
|
}
|
|
7007
|
-
return
|
|
7322
|
+
return staged;
|
|
7323
|
+
}
|
|
7324
|
+
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
7325
|
+
const db = getDb();
|
|
7326
|
+
for (const item of staged) {
|
|
7327
|
+
const entry = remoteLock.hooks[item.name];
|
|
7328
|
+
if (!entry)
|
|
7329
|
+
continue;
|
|
7330
|
+
writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
|
|
7331
|
+
const scriptPath = resolveScriptPath(item.name);
|
|
7332
|
+
const actual = await sha256File(scriptPath);
|
|
7333
|
+
if (actual !== entry.sha256) {
|
|
7334
|
+
throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
const lock = readLock();
|
|
7338
|
+
for (const item of staged) {
|
|
7339
|
+
const entry = remoteLock.hooks[item.name];
|
|
7340
|
+
if (!entry)
|
|
7341
|
+
continue;
|
|
7342
|
+
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
7343
|
+
}
|
|
7344
|
+
writeLock(lock);
|
|
7345
|
+
db.exec("BEGIN");
|
|
7346
|
+
try {
|
|
7347
|
+
for (const item of staged) {
|
|
7348
|
+
const entry = remoteLock.hooks[item.name];
|
|
7349
|
+
if (!entry)
|
|
7350
|
+
continue;
|
|
7351
|
+
upsertHookRecord(db, {
|
|
7352
|
+
name: item.name,
|
|
7353
|
+
version: entry.version,
|
|
7354
|
+
sha256: entry.sha256,
|
|
7355
|
+
source_type: entry.source ?? "remote",
|
|
7356
|
+
source_ref: apiUrl,
|
|
7357
|
+
last_verified_at: new Date().toISOString()
|
|
7358
|
+
});
|
|
7359
|
+
}
|
|
7360
|
+
db.exec("COMMIT");
|
|
7361
|
+
} catch (err) {
|
|
7362
|
+
db.exec("ROLLBACK");
|
|
7363
|
+
throw err;
|
|
7364
|
+
}
|
|
7008
7365
|
}
|
|
7009
7366
|
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7010
7367
|
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
@@ -7012,10 +7369,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7012
7369
|
if (!entry) {
|
|
7013
7370
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
7014
7371
|
}
|
|
7015
|
-
|
|
7016
|
-
|
|
7372
|
+
const isLatest = entry.version === version;
|
|
7373
|
+
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7374
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
7017
7375
|
}
|
|
7018
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}
|
|
7376
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
7019
7377
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7020
7378
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7021
7379
|
}
|
|
@@ -7027,24 +7385,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7027
7385
|
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
7028
7386
|
}
|
|
7029
7387
|
const actualSha = sha256OfText(artifact.script);
|
|
7030
|
-
|
|
7031
|
-
|
|
7388
|
+
let expectedSha = entry.sha256;
|
|
7389
|
+
if (!isLatest) {
|
|
7390
|
+
const headerSha = artifact._headerSha;
|
|
7391
|
+
if (!headerSha) {
|
|
7392
|
+
throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
|
|
7393
|
+
}
|
|
7394
|
+
expectedSha = headerSha;
|
|
7395
|
+
}
|
|
7396
|
+
if (actualSha !== expectedSha) {
|
|
7397
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
|
|
7032
7398
|
}
|
|
7033
7399
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
7034
|
-
const scriptRel =
|
|
7035
|
-
`) ? "script.ts" : artifact.manifest.script;
|
|
7400
|
+
const scriptRel = scriptRelFor(manifest);
|
|
7036
7401
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
7037
7402
|
const scriptPath = resolveScriptPath(name);
|
|
7038
7403
|
const actual = await sha256File(scriptPath);
|
|
7039
|
-
if (actual !==
|
|
7040
|
-
throw new Error(`sha256 mismatch after write for '${name}@${version}':
|
|
7404
|
+
if (actual !== expectedSha) {
|
|
7405
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
7041
7406
|
}
|
|
7042
7407
|
const db = getDb();
|
|
7043
|
-
setPinnedHook(name, { version, sha256:
|
|
7408
|
+
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
7044
7409
|
upsertHookRecord(db, {
|
|
7045
7410
|
name,
|
|
7046
7411
|
version,
|
|
7047
|
-
sha256:
|
|
7412
|
+
sha256: expectedSha,
|
|
7048
7413
|
source_type: entry.source ?? "remote",
|
|
7049
7414
|
source_ref: apiUrl,
|
|
7050
7415
|
last_verified_at: new Date().toISOString()
|
|
@@ -7052,7 +7417,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7052
7417
|
return {
|
|
7053
7418
|
name,
|
|
7054
7419
|
version,
|
|
7055
|
-
sha256:
|
|
7420
|
+
sha256: expectedSha,
|
|
7056
7421
|
source: entry.source ?? "remote",
|
|
7057
7422
|
source_ref: apiUrl,
|
|
7058
7423
|
artifact,
|
|
@@ -7078,6 +7443,7 @@ function secureEqual(a, b) {
|
|
|
7078
7443
|
}
|
|
7079
7444
|
|
|
7080
7445
|
// src/serve.ts
|
|
7446
|
+
init_semver();
|
|
7081
7447
|
var DEFAULT_SERVE_PORT = 39428;
|
|
7082
7448
|
var SERVE_HOST = "127.0.0.1";
|
|
7083
7449
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
@@ -7087,24 +7453,29 @@ async function buildCatalog() {
|
|
|
7087
7453
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
7088
7454
|
if (!scriptPath)
|
|
7089
7455
|
continue;
|
|
7456
|
+
const sha = await sha256File(scriptPath);
|
|
7090
7457
|
byName.set(meta.name, {
|
|
7091
7458
|
name: meta.name,
|
|
7092
7459
|
version: meta.version,
|
|
7093
|
-
sha256:
|
|
7460
|
+
sha256: sha,
|
|
7094
7461
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
7095
7462
|
description: meta.description,
|
|
7096
|
-
source: "bundled"
|
|
7463
|
+
source: "bundled",
|
|
7464
|
+
versions: [meta.version]
|
|
7097
7465
|
});
|
|
7098
7466
|
}
|
|
7099
7467
|
for (const custom2 of listCustomHooks()) {
|
|
7100
7468
|
const name = shortManifestName(custom2.manifest.name);
|
|
7469
|
+
const sha = await sha256File(custom2.scriptPath);
|
|
7470
|
+
const existing = byName.get(name);
|
|
7101
7471
|
byName.set(name, {
|
|
7102
7472
|
name,
|
|
7103
7473
|
version: custom2.manifest.version,
|
|
7104
|
-
sha256:
|
|
7474
|
+
sha256: sha,
|
|
7105
7475
|
events: custom2.manifest.events,
|
|
7106
7476
|
description: custom2.manifest.description ?? "Custom hook",
|
|
7107
|
-
source:
|
|
7477
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
7478
|
+
versions: existing ? [...new Set([...existing.versions, custom2.manifest.version])] : [custom2.manifest.version]
|
|
7108
7479
|
});
|
|
7109
7480
|
}
|
|
7110
7481
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -7122,6 +7493,7 @@ async function artifactFor(name, version) {
|
|
|
7122
7493
|
description: resolved.description,
|
|
7123
7494
|
events: resolved.events,
|
|
7124
7495
|
script: custom2 ? custom2.manifest.script : `src/hook.ts`,
|
|
7496
|
+
script_kind: custom2?.manifest.script_kind,
|
|
7125
7497
|
args: custom2?.manifest.args,
|
|
7126
7498
|
timeout_ms: custom2?.manifest.timeout_ms
|
|
7127
7499
|
};
|
|
@@ -7151,11 +7523,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
7151
7523
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
7152
7524
|
}
|
|
7153
7525
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
7154
|
-
return
|
|
7526
|
+
return buildCatalog().then((catalog) => {
|
|
7527
|
+
const lock = readLock();
|
|
7528
|
+
const byVersion = {};
|
|
7529
|
+
for (const entry of catalog)
|
|
7530
|
+
byVersion[entry.name] = entry.versions;
|
|
7531
|
+
const hooks = {};
|
|
7532
|
+
for (const [name, pin] of Object.entries(lock.hooks)) {
|
|
7533
|
+
hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
|
|
7534
|
+
}
|
|
7535
|
+
return json({ hooks });
|
|
7536
|
+
});
|
|
7155
7537
|
}
|
|
7156
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
7538
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
7157
7539
|
if (artifactMatch && req.method === "GET") {
|
|
7158
|
-
const [,
|
|
7540
|
+
const [, rawName, rawVersion] = artifactMatch;
|
|
7541
|
+
let name;
|
|
7542
|
+
let version;
|
|
7543
|
+
try {
|
|
7544
|
+
name = decodeURIComponent(rawName);
|
|
7545
|
+
version = decodeURIComponent(rawVersion);
|
|
7546
|
+
} catch {
|
|
7547
|
+
return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
|
|
7548
|
+
}
|
|
7549
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
7550
|
+
return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
|
|
7551
|
+
}
|
|
7159
7552
|
return artifactFor(name, version).then(async (artifact) => {
|
|
7160
7553
|
if (!artifact)
|
|
7161
7554
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -7186,7 +7579,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
7186
7579
|
function startServeServer(options) {
|
|
7187
7580
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
7188
7581
|
const host = options.host ?? SERVE_HOST;
|
|
7189
|
-
const apiKey = resolveApiKey(
|
|
7582
|
+
const apiKey = resolveApiKey();
|
|
7190
7583
|
const server = Bun.serve({
|
|
7191
7584
|
hostname: host,
|
|
7192
7585
|
port,
|
|
@@ -7296,7 +7689,7 @@ async function runHook(name, input, options = {}) {
|
|
|
7296
7689
|
throw new Error(`Hook '${name}' not found`);
|
|
7297
7690
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
7298
7691
|
const resolvedScript = custom2?.scriptPath;
|
|
7299
|
-
const script = resolvedScript ?? (
|
|
7692
|
+
const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
|
|
7300
7693
|
if (!script)
|
|
7301
7694
|
throw new Error(`Hook script not found: ${name}`);
|
|
7302
7695
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
@@ -7331,8 +7724,8 @@ async function runHook(name, input, options = {}) {
|
|
|
7331
7724
|
scriptPath: script,
|
|
7332
7725
|
content,
|
|
7333
7726
|
args: custom2?.manifest.args ?? [],
|
|
7727
|
+
env: custom2?.manifest.env,
|
|
7334
7728
|
stdin: JSON.stringify(hookInput),
|
|
7335
|
-
env: process.env,
|
|
7336
7729
|
timeout: effectiveTimeout ?? undefined
|
|
7337
7730
|
}));
|
|
7338
7731
|
} catch (err) {
|