@hasna/hooks 0.6.4 → 0.6.6
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 +619 -174
- package/bin/serve.js +115 -35
- package/dist/config.d.ts +8 -1
- package/dist/db/remote-storage.d.ts +22 -0
- package/dist/index.js +442 -121
- package/dist/lib/hook-env.d.ts +42 -0
- package/dist/lib/manifest.d.ts +24 -2
- package/dist/lib/redact.d.ts +40 -0
- package/dist/lib/semver.d.ts +19 -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,7 +399,61 @@ var init_db = __esm(() => {
|
|
|
399
399
|
init_legacy_import();
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
-
//
|
|
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
|
+
|
|
456
|
+
// node_modules/zod/v3/helpers/util.js
|
|
403
457
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
404
458
|
const t = typeof data;
|
|
405
459
|
switch (t) {
|
|
@@ -530,7 +584,7 @@ var init_util = __esm(() => {
|
|
|
530
584
|
]);
|
|
531
585
|
});
|
|
532
586
|
|
|
533
|
-
//
|
|
587
|
+
// node_modules/zod/v3/ZodError.js
|
|
534
588
|
var ZodIssueCode, quotelessJson = (obj) => {
|
|
535
589
|
const json = JSON.stringify(obj, null, 2);
|
|
536
590
|
return json.replace(/"([^"]+)":/g, "$1:");
|
|
@@ -651,7 +705,7 @@ var init_ZodError = __esm(() => {
|
|
|
651
705
|
};
|
|
652
706
|
});
|
|
653
707
|
|
|
654
|
-
//
|
|
708
|
+
// node_modules/zod/v3/locales/en.js
|
|
655
709
|
var errorMap = (issue, _ctx) => {
|
|
656
710
|
let message;
|
|
657
711
|
switch (issue.code) {
|
|
@@ -758,7 +812,7 @@ var init_en = __esm(() => {
|
|
|
758
812
|
en_default = errorMap;
|
|
759
813
|
});
|
|
760
814
|
|
|
761
|
-
//
|
|
815
|
+
// node_modules/zod/v3/errors.js
|
|
762
816
|
function setErrorMap(map) {
|
|
763
817
|
overrideErrorMap = map;
|
|
764
818
|
}
|
|
@@ -771,7 +825,7 @@ var init_errors = __esm(() => {
|
|
|
771
825
|
overrideErrorMap = en_default;
|
|
772
826
|
});
|
|
773
827
|
|
|
774
|
-
//
|
|
828
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
775
829
|
function addIssueToContext(ctx, issueData) {
|
|
776
830
|
const overrideMap = getErrorMap();
|
|
777
831
|
const issue = makeIssue({
|
|
@@ -876,10 +930,10 @@ var init_parseUtil = __esm(() => {
|
|
|
876
930
|
});
|
|
877
931
|
});
|
|
878
932
|
|
|
879
|
-
//
|
|
933
|
+
// node_modules/zod/v3/helpers/typeAliases.js
|
|
880
934
|
var init_typeAliases = () => {};
|
|
881
935
|
|
|
882
|
-
//
|
|
936
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
883
937
|
var errorUtil;
|
|
884
938
|
var init_errorUtil = __esm(() => {
|
|
885
939
|
(function(errorUtil2) {
|
|
@@ -888,7 +942,7 @@ var init_errorUtil = __esm(() => {
|
|
|
888
942
|
})(errorUtil || (errorUtil = {}));
|
|
889
943
|
});
|
|
890
944
|
|
|
891
|
-
//
|
|
945
|
+
// node_modules/zod/v3/types.js
|
|
892
946
|
class ParseInputLazyPath {
|
|
893
947
|
constructor(parent, value, path, key) {
|
|
894
948
|
this._cachedPath = [];
|
|
@@ -4239,7 +4293,7 @@ var init_types = __esm(() => {
|
|
|
4239
4293
|
NEVER = INVALID;
|
|
4240
4294
|
});
|
|
4241
4295
|
|
|
4242
|
-
//
|
|
4296
|
+
// node_modules/zod/v3/external.js
|
|
4243
4297
|
var exports_external = {};
|
|
4244
4298
|
__export(exports_external, {
|
|
4245
4299
|
void: () => voidType,
|
|
@@ -4359,7 +4413,7 @@ var init_external = __esm(() => {
|
|
|
4359
4413
|
init_ZodError();
|
|
4360
4414
|
});
|
|
4361
4415
|
|
|
4362
|
-
//
|
|
4416
|
+
// node_modules/zod/index.js
|
|
4363
4417
|
var init_zod = __esm(() => {
|
|
4364
4418
|
init_external();
|
|
4365
4419
|
init_external();
|
|
@@ -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 SEMVER_PATTERN;
|
|
4478
|
+
var init_semver = __esm(() => {
|
|
4479
|
+
SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[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,13 +4628,15 @@ 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
4641
|
timeout_ms: exports_external.number().int().positive().optional()
|
|
4571
4642
|
});
|
|
@@ -4595,11 +4666,13 @@ __export(exports_store, {
|
|
|
4595
4666
|
listHookRecords: () => listHookRecords,
|
|
4596
4667
|
getPinnedHook: () => getPinnedHook,
|
|
4597
4668
|
getHookRecord: () => getHookRecord,
|
|
4598
|
-
checkScriptHash: () => checkScriptHash
|
|
4669
|
+
checkScriptHash: () => checkScriptHash,
|
|
4670
|
+
LockFileError: () => LockFileError
|
|
4599
4671
|
});
|
|
4600
4672
|
import { createHash } from "crypto";
|
|
4601
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4673
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4602
4674
|
import { readFile } from "fs/promises";
|
|
4675
|
+
import { randomBytes } from "crypto";
|
|
4603
4676
|
function sha256Of(input) {
|
|
4604
4677
|
return createHash("sha256").update(input).digest("hex");
|
|
4605
4678
|
}
|
|
@@ -4642,16 +4715,39 @@ function removeHookRecord(db, name) {
|
|
|
4642
4715
|
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
4643
4716
|
return res.changes > 0;
|
|
4644
4717
|
}
|
|
4718
|
+
function assertLockShape(parsed, path) {
|
|
4719
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4720
|
+
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.`);
|
|
4721
|
+
}
|
|
4722
|
+
const hooks = parsed.hooks;
|
|
4723
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
4724
|
+
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.`);
|
|
4725
|
+
}
|
|
4726
|
+
for (const [name, entry] of Object.entries(hooks)) {
|
|
4727
|
+
const e = entry;
|
|
4728
|
+
if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
|
|
4729
|
+
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.`);
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4645
4733
|
function readLock() {
|
|
4734
|
+
const path = getLockPath();
|
|
4735
|
+
if (!existsSync6(path))
|
|
4736
|
+
return { hooks: {} };
|
|
4737
|
+
let raw;
|
|
4646
4738
|
try {
|
|
4647
|
-
|
|
4648
|
-
if (!existsSync6(path))
|
|
4649
|
-
return { hooks: {} };
|
|
4650
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4651
|
-
return { hooks: parsed.hooks ?? {} };
|
|
4739
|
+
raw = readFileSync4(path, "utf-8");
|
|
4652
4740
|
} catch {
|
|
4653
|
-
|
|
4741
|
+
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4742
|
+
}
|
|
4743
|
+
let parsed;
|
|
4744
|
+
try {
|
|
4745
|
+
parsed = JSON.parse(raw);
|
|
4746
|
+
} catch {
|
|
4747
|
+
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
4748
|
}
|
|
4749
|
+
assertLockShape(parsed, path);
|
|
4750
|
+
return parsed;
|
|
4655
4751
|
}
|
|
4656
4752
|
function writeLock(lock) {
|
|
4657
4753
|
const path = getLockPath();
|
|
@@ -4660,8 +4756,10 @@ function writeLock(lock) {
|
|
|
4660
4756
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4661
4757
|
sorted[name] = lock.hooks[name];
|
|
4662
4758
|
}
|
|
4663
|
-
|
|
4759
|
+
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
4760
|
+
writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4664
4761
|
`, "utf-8");
|
|
4762
|
+
renameSync(tmp, path);
|
|
4665
4763
|
return path;
|
|
4666
4764
|
}
|
|
4667
4765
|
function setPinnedHook(name, entry) {
|
|
@@ -4747,9 +4845,16 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
4747
4845
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
4748
4846
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
4749
4847
|
}
|
|
4848
|
+
var LockFileError;
|
|
4750
4849
|
var init_store = __esm(() => {
|
|
4751
4850
|
init_db();
|
|
4752
4851
|
init_config();
|
|
4852
|
+
LockFileError = class LockFileError extends Error {
|
|
4853
|
+
constructor(message) {
|
|
4854
|
+
super(message);
|
|
4855
|
+
this.name = "LockFileError";
|
|
4856
|
+
}
|
|
4857
|
+
};
|
|
4753
4858
|
});
|
|
4754
4859
|
|
|
4755
4860
|
// src/lib/profiles.ts
|
|
@@ -4895,13 +5000,73 @@ var init_profiles = __esm(() => {
|
|
|
4895
5000
|
PROFILES_DIR = resolveProfilesDir();
|
|
4896
5001
|
});
|
|
4897
5002
|
|
|
5003
|
+
// src/lib/hook-env.ts
|
|
5004
|
+
function isDeniedEnvName(name) {
|
|
5005
|
+
const upper = name.toUpperCase();
|
|
5006
|
+
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
5007
|
+
return true;
|
|
5008
|
+
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
5009
|
+
return true;
|
|
5010
|
+
return DENY_CONTAINS.some((token) => upper.includes(token));
|
|
5011
|
+
}
|
|
5012
|
+
function buildHookEnv(source, extra) {
|
|
5013
|
+
const out = {};
|
|
5014
|
+
for (const name of ALLOWLIST) {
|
|
5015
|
+
const value = source[name] ?? extra?.[name];
|
|
5016
|
+
if (value !== undefined && value !== null)
|
|
5017
|
+
out[name] = value;
|
|
5018
|
+
}
|
|
5019
|
+
for (const [from, to] of CONFIG_PROJECTIONS) {
|
|
5020
|
+
const value = source[from] ?? extra?.[from];
|
|
5021
|
+
if (value !== undefined && value !== null)
|
|
5022
|
+
out[to] = value;
|
|
5023
|
+
}
|
|
5024
|
+
const merged = { ...source, ...extra ?? {} };
|
|
5025
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
5026
|
+
if (out[name] !== undefined)
|
|
5027
|
+
continue;
|
|
5028
|
+
if (isDeniedEnvName(name))
|
|
5029
|
+
continue;
|
|
5030
|
+
if (value !== undefined && value !== null)
|
|
5031
|
+
out[name] = value;
|
|
5032
|
+
}
|
|
5033
|
+
return out;
|
|
5034
|
+
}
|
|
5035
|
+
var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
|
|
5036
|
+
var init_hook_env = __esm(() => {
|
|
5037
|
+
ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
|
|
5038
|
+
DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
|
|
5039
|
+
DENY_PREFIXES = [
|
|
5040
|
+
"HASNA_",
|
|
5041
|
+
"AWS_",
|
|
5042
|
+
"AZURE_",
|
|
5043
|
+
"GCP_",
|
|
5044
|
+
"VAULT_",
|
|
5045
|
+
"GOOGLE_",
|
|
5046
|
+
"OPENAI_",
|
|
5047
|
+
"ANTHROPIC_",
|
|
5048
|
+
"POSTGRES_",
|
|
5049
|
+
"MYSQL_",
|
|
5050
|
+
"REDIS_",
|
|
5051
|
+
"MONGO_",
|
|
5052
|
+
"MEMENTOS_"
|
|
5053
|
+
];
|
|
5054
|
+
DENY_CONTAINS = ["DATABASE_URL"];
|
|
5055
|
+
CONFIG_PROJECTIONS = [
|
|
5056
|
+
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
5057
|
+
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
5058
|
+
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
5059
|
+
["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
|
|
5060
|
+
];
|
|
5061
|
+
});
|
|
5062
|
+
|
|
4898
5063
|
// src/lib/run.ts
|
|
4899
5064
|
var exports_run = {};
|
|
4900
5065
|
__export(exports_run, {
|
|
4901
5066
|
executeVerifiedScript: () => executeVerifiedScript,
|
|
4902
5067
|
HookTimeoutError: () => HookTimeoutError
|
|
4903
5068
|
});
|
|
4904
|
-
import { randomBytes } from "crypto";
|
|
5069
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4905
5070
|
import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4906
5071
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
4907
5072
|
function extensionOf(scriptPath) {
|
|
@@ -4974,12 +5139,12 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
4974
5139
|
async function executeVerifiedScript(options) {
|
|
4975
5140
|
const scriptDir = dirname4(options.scriptPath);
|
|
4976
5141
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
4977
|
-
const tempPath = join9(scriptDir, `.hooks-verified-${
|
|
5142
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
4978
5143
|
let proc = null;
|
|
4979
5144
|
let timedOut = false;
|
|
4980
5145
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
4981
5146
|
timedOut = true;
|
|
4982
|
-
killGroup(proc);
|
|
5147
|
+
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
4983
5148
|
}, options.timeout) : null;
|
|
4984
5149
|
try {
|
|
4985
5150
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
@@ -4987,7 +5152,7 @@ async function executeVerifiedScript(options) {
|
|
|
4987
5152
|
stdin: new Response(options.stdin),
|
|
4988
5153
|
stdout: "pipe",
|
|
4989
5154
|
stderr: "pipe",
|
|
4990
|
-
env:
|
|
5155
|
+
env: buildHookEnv(process.env, options.env),
|
|
4991
5156
|
detached: true
|
|
4992
5157
|
});
|
|
4993
5158
|
const exitCode = await proc.exited;
|
|
@@ -4998,7 +5163,7 @@ async function executeVerifiedScript(options) {
|
|
|
4998
5163
|
readPipeWithDeadline(proc.stdout, 750),
|
|
4999
5164
|
readPipeWithDeadline(proc.stderr, 750)
|
|
5000
5165
|
]);
|
|
5001
|
-
killGroup(proc);
|
|
5166
|
+
killGroup(proc, { leaderAlive: false });
|
|
5002
5167
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5003
5168
|
} finally {
|
|
5004
5169
|
if (timeoutTimer)
|
|
@@ -5008,12 +5173,36 @@ async function executeVerifiedScript(options) {
|
|
|
5008
5173
|
} catch {}
|
|
5009
5174
|
}
|
|
5010
5175
|
}
|
|
5011
|
-
function killGroup(proc) {
|
|
5176
|
+
function killGroup(proc, options) {
|
|
5012
5177
|
if (!proc)
|
|
5013
5178
|
return;
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5179
|
+
const pgid = proc.pid;
|
|
5180
|
+
for (let pass = 0;pass < 5; pass++) {
|
|
5181
|
+
let members = [];
|
|
5182
|
+
try {
|
|
5183
|
+
const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
|
|
5184
|
+
if (res.exitCode === 0) {
|
|
5185
|
+
members = res.stdout.toString().split(`
|
|
5186
|
+
`).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length === 2 && cols[1] === String(pgid)).map((cols) => Number(cols[0])).filter((pid) => Number.isInteger(pid) && pid > 1).filter((pid) => options.leaderAlive || pid !== proc.pid);
|
|
5187
|
+
}
|
|
5188
|
+
} catch {}
|
|
5189
|
+
if (members.length === 0)
|
|
5190
|
+
break;
|
|
5191
|
+
let killed = 0;
|
|
5192
|
+
for (const pid of members) {
|
|
5193
|
+
try {
|
|
5194
|
+
const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
5195
|
+
const cols = probe.stdout.toString().trim().split(/\s+/);
|
|
5196
|
+
if (cols.length === 2 && cols[1] === String(pgid)) {
|
|
5197
|
+
process.kill(pid, "SIGKILL");
|
|
5198
|
+
killed++;
|
|
5199
|
+
}
|
|
5200
|
+
} catch {}
|
|
5201
|
+
}
|
|
5202
|
+
if (killed === 0)
|
|
5203
|
+
break;
|
|
5204
|
+
}
|
|
5205
|
+
if (options.leaderAlive) {
|
|
5017
5206
|
try {
|
|
5018
5207
|
process.kill(proc.pid, "SIGKILL");
|
|
5019
5208
|
} catch {}
|
|
@@ -5021,6 +5210,7 @@ function killGroup(proc) {
|
|
|
5021
5210
|
}
|
|
5022
5211
|
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5023
5212
|
var init_run = __esm(() => {
|
|
5213
|
+
init_hook_env();
|
|
5024
5214
|
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5025
5215
|
timeoutMs;
|
|
5026
5216
|
constructor(timeoutMs) {
|
|
@@ -5049,6 +5239,9 @@ function writeHookEvent(event) {
|
|
|
5049
5239
|
const db = getDb();
|
|
5050
5240
|
const id = nanoid2();
|
|
5051
5241
|
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5242
|
+
const toolInput = redactEventPayload(event.tool_input);
|
|
5243
|
+
const error = redactEventPayload(event.error);
|
|
5244
|
+
const metadata = redactEventPayload(event.metadata);
|
|
5052
5245
|
db.run(`INSERT INTO hook_events
|
|
5053
5246
|
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5054
5247
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
@@ -5058,12 +5251,12 @@ function writeHookEvent(event) {
|
|
|
5058
5251
|
event.hook_name,
|
|
5059
5252
|
event.event_type,
|
|
5060
5253
|
event.tool_name ?? null,
|
|
5061
|
-
|
|
5254
|
+
toolInput ? toolInput.slice(0, 500) : null,
|
|
5062
5255
|
event.result ?? null,
|
|
5063
|
-
|
|
5256
|
+
error ?? null,
|
|
5064
5257
|
event.duration_ms ?? null,
|
|
5065
5258
|
event.project_dir ?? null,
|
|
5066
|
-
|
|
5259
|
+
metadata ?? null
|
|
5067
5260
|
]);
|
|
5068
5261
|
} catch (err) {
|
|
5069
5262
|
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
@@ -5113,6 +5306,7 @@ function recordHookRun(record) {
|
|
|
5113
5306
|
var ALLOWED_EVENT_TYPES;
|
|
5114
5307
|
var init_db_writer = __esm(() => {
|
|
5115
5308
|
init_db();
|
|
5309
|
+
init_redact();
|
|
5116
5310
|
ALLOWED_EVENT_TYPES = new Set([
|
|
5117
5311
|
"PreToolUse",
|
|
5118
5312
|
"PostToolUse",
|
|
@@ -5135,7 +5329,7 @@ var PG_MIGRATIONS = [
|
|
|
5135
5329
|
timestamp TEXT NOT NULL,
|
|
5136
5330
|
session_id TEXT NOT NULL,
|
|
5137
5331
|
hook_name TEXT NOT NULL,
|
|
5138
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
5332
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
5139
5333
|
tool_name TEXT,
|
|
5140
5334
|
tool_input TEXT,
|
|
5141
5335
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -5168,7 +5362,10 @@ var PG_MIGRATIONS = [
|
|
|
5168
5362
|
)`,
|
|
5169
5363
|
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
5170
5364
|
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
5171
|
-
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))
|
|
5365
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
|
|
5366
|
+
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
5367
|
+
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
5368
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
|
|
5172
5369
|
];
|
|
5173
5370
|
|
|
5174
5371
|
// src/db/remote-storage.ts
|
|
@@ -5182,7 +5379,34 @@ function normalizeParams(params) {
|
|
|
5182
5379
|
return flat.map((value) => value === undefined ? null : value);
|
|
5183
5380
|
}
|
|
5184
5381
|
function sslConfigFor(connectionString) {
|
|
5185
|
-
|
|
5382
|
+
const wantsSsl = wantsSslFor(connectionString);
|
|
5383
|
+
if (!wantsSsl)
|
|
5384
|
+
return;
|
|
5385
|
+
const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
|
|
5386
|
+
if (insecure && false) {}
|
|
5387
|
+
return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
|
|
5388
|
+
}
|
|
5389
|
+
function wantsSslFor(connectionString) {
|
|
5390
|
+
let raw;
|
|
5391
|
+
try {
|
|
5392
|
+
raw = new URL(connectionString).search.replace(/^\?/, "");
|
|
5393
|
+
} catch {
|
|
5394
|
+
raw = connectionString;
|
|
5395
|
+
}
|
|
5396
|
+
try {
|
|
5397
|
+
raw = decodeURIComponent(raw);
|
|
5398
|
+
} catch {}
|
|
5399
|
+
const paramValue = (name) => {
|
|
5400
|
+
const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
|
|
5401
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
5402
|
+
};
|
|
5403
|
+
const sslmode = paramValue("sslmode");
|
|
5404
|
+
if (sslmode !== undefined)
|
|
5405
|
+
return sslmode !== "disable" && sslmode !== "allow";
|
|
5406
|
+
const ssl = paramValue("ssl");
|
|
5407
|
+
if (ssl !== undefined)
|
|
5408
|
+
return ssl !== "false" && ssl !== "0" && ssl !== "no";
|
|
5409
|
+
return false;
|
|
5186
5410
|
}
|
|
5187
5411
|
|
|
5188
5412
|
class PgAdapterAsync {
|
|
@@ -5204,6 +5428,7 @@ class PgAdapterAsync {
|
|
|
5204
5428
|
}
|
|
5205
5429
|
|
|
5206
5430
|
// src/db/storage-sync.ts
|
|
5431
|
+
init_redact();
|
|
5207
5432
|
var STORAGE_TABLES = [
|
|
5208
5433
|
"hook_events",
|
|
5209
5434
|
"schema_migrations",
|
|
@@ -5380,9 +5605,15 @@ async function pushTable(db, remote, table) {
|
|
|
5380
5605
|
result.rowsRead = rows.length;
|
|
5381
5606
|
if (rows.length === 0)
|
|
5382
5607
|
return result;
|
|
5608
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
5609
|
+
...row,
|
|
5610
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
5611
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
5612
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
5613
|
+
})) : rows;
|
|
5383
5614
|
const remoteColumns = await getRemoteColumns(remote, table);
|
|
5384
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(
|
|
5385
|
-
result.rowsWritten = await upsertPg(remote, table, columns,
|
|
5615
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
|
|
5616
|
+
result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
|
|
5386
5617
|
} catch (error) {
|
|
5387
5618
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
5388
5619
|
}
|
|
@@ -5397,8 +5628,14 @@ async function pullTable(remote, db, table) {
|
|
|
5397
5628
|
result.rowsRead = rows.length;
|
|
5398
5629
|
if (rows.length === 0)
|
|
5399
5630
|
return result;
|
|
5400
|
-
const
|
|
5401
|
-
|
|
5631
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
5632
|
+
...row,
|
|
5633
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
5634
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
5635
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
5636
|
+
})) : rows;
|
|
5637
|
+
const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
|
|
5638
|
+
result.rowsWritten = upsertSqlite(db, table, columns, projected);
|
|
5402
5639
|
} catch (error) {
|
|
5403
5640
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
5404
5641
|
}
|
|
@@ -6212,7 +6449,7 @@ function getGlobalSettingsPathOverride(target) {
|
|
|
6212
6449
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
6213
6450
|
if (target === "codewith" && codewithConfigPath)
|
|
6214
6451
|
return codewithConfigPath;
|
|
6215
|
-
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
6452
|
+
if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
6216
6453
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
6217
6454
|
}
|
|
6218
6455
|
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
@@ -6646,8 +6883,8 @@ function escapeRegExp(value) {
|
|
|
6646
6883
|
function codewithHasHookEntryInText(configText, name) {
|
|
6647
6884
|
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
6648
6885
|
}
|
|
6649
|
-
function codewithHasHookEntry(name) {
|
|
6650
|
-
const path = getSettingsPath(
|
|
6886
|
+
function codewithHasHookEntry(name, scope = "global") {
|
|
6887
|
+
const path = getSettingsPath(scope, "codewith");
|
|
6651
6888
|
if (!existsSync7(path))
|
|
6652
6889
|
return false;
|
|
6653
6890
|
try {
|
|
@@ -6664,7 +6901,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6664
6901
|
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
6665
6902
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
6666
6903
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
6667
|
-
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
6904
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
6668
6905
|
if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
6669
6906
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
6670
6907
|
}
|
|
@@ -6681,7 +6918,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6681
6918
|
}
|
|
6682
6919
|
const registrationsRemaining = [];
|
|
6683
6920
|
if (target === "codewith" || target === "all") {
|
|
6684
|
-
const codewithPath = getSettingsPath(
|
|
6921
|
+
const codewithPath = getSettingsPath(scope, "codewith");
|
|
6685
6922
|
if (existsSync7(codewithPath)) {
|
|
6686
6923
|
const before = readFileSync5(codewithPath, "utf-8");
|
|
6687
6924
|
const after = removeCodewithHookEntry(before, shortName);
|
|
@@ -6807,19 +7044,20 @@ async function installCustomSource(source) {
|
|
|
6807
7044
|
return copyManifestDir(source, source, "local");
|
|
6808
7045
|
}
|
|
6809
7046
|
async function installManifestUrl(url) {
|
|
6810
|
-
const res = await fetch(url, { redirect: "
|
|
7047
|
+
const res = await fetch(url, { redirect: "error" });
|
|
6811
7048
|
if (!res.ok) {
|
|
6812
7049
|
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6813
7050
|
}
|
|
6814
7051
|
const manifest = parseManifest(await res.text());
|
|
6815
7052
|
let script;
|
|
6816
|
-
|
|
6817
|
-
`)
|
|
7053
|
+
const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
7054
|
+
`);
|
|
7055
|
+
if (inline) {
|
|
6818
7056
|
script = { path: "script.ts", content: manifest.script };
|
|
6819
7057
|
} else {
|
|
6820
7058
|
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6821
7059
|
const scriptUrl = `${base}/${manifest.script}`;
|
|
6822
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "
|
|
7060
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "error" });
|
|
6823
7061
|
if (!scriptRes.ok) {
|
|
6824
7062
|
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6825
7063
|
}
|
|
@@ -6856,7 +7094,7 @@ function collectBundledCatalog() {
|
|
|
6856
7094
|
}
|
|
6857
7095
|
return out;
|
|
6858
7096
|
}
|
|
6859
|
-
async function fetchJson(base, path) {
|
|
7097
|
+
async function fetchJson(base, path, opts) {
|
|
6860
7098
|
const headers = { accept: "application/json" };
|
|
6861
7099
|
const apiKey = resolveApiKey();
|
|
6862
7100
|
if (apiKey)
|
|
@@ -6872,7 +7110,11 @@ async function fetchJson(base, path) {
|
|
|
6872
7110
|
if (!res.ok) {
|
|
6873
7111
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6874
7112
|
}
|
|
6875
|
-
|
|
7113
|
+
const body = await res.json();
|
|
7114
|
+
if (opts?.withHeader) {
|
|
7115
|
+
body[opts.withHeader] = res.headers.get("x-hook-sha256");
|
|
7116
|
+
}
|
|
7117
|
+
return body;
|
|
6876
7118
|
}
|
|
6877
7119
|
async function fetchRemoteState(apiUrl) {
|
|
6878
7120
|
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
@@ -6885,6 +7127,16 @@ async function fetchRemoteState(apiUrl) {
|
|
|
6885
7127
|
}
|
|
6886
7128
|
return { catalog: catalog.hooks, remoteLock };
|
|
6887
7129
|
}
|
|
7130
|
+
function classifyRemotePin(pin, remoteVersion, remoteSha) {
|
|
7131
|
+
const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
|
|
7132
|
+
if (pinnedLocally)
|
|
7133
|
+
return "unchanged";
|
|
7134
|
+
if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
|
|
7135
|
+
return "unchanged";
|
|
7136
|
+
if (pin !== undefined)
|
|
7137
|
+
return "updated";
|
|
7138
|
+
return "added";
|
|
7139
|
+
}
|
|
6888
7140
|
function computeDiff(catalog, remoteLock) {
|
|
6889
7141
|
const local = readLock();
|
|
6890
7142
|
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
@@ -6892,30 +7144,30 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6892
7144
|
if (!entry.name || !entry.version || !entry.sha256)
|
|
6893
7145
|
continue;
|
|
6894
7146
|
const pin = local.hooks[entry.name];
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
diff.
|
|
7147
|
+
if (remoteLock) {
|
|
7148
|
+
const remotePin = remoteLock.hooks[entry.name];
|
|
7149
|
+
if (!remotePin) {
|
|
7150
|
+
diff.skipped.push(entry.name);
|
|
7151
|
+
continue;
|
|
7152
|
+
}
|
|
7153
|
+
if (remotePin.version !== entry.version) {
|
|
7154
|
+
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.`);
|
|
7155
|
+
}
|
|
7156
|
+
diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
|
|
6905
7157
|
} else {
|
|
6906
|
-
diff.
|
|
7158
|
+
diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
|
|
6907
7159
|
}
|
|
6908
7160
|
}
|
|
6909
7161
|
return diff;
|
|
6910
7162
|
}
|
|
6911
|
-
async function planSync() {
|
|
7163
|
+
async function planSync(options = {}) {
|
|
6912
7164
|
const apiUrl = resolveApiUrl();
|
|
6913
7165
|
if (apiUrl) {
|
|
6914
7166
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6915
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
7167
|
+
return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6916
7168
|
}
|
|
6917
7169
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6918
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
7170
|
+
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6919
7171
|
}
|
|
6920
7172
|
async function syncHooks(options = {}) {
|
|
6921
7173
|
const apiUrl = resolveApiUrl();
|
|
@@ -6923,10 +7175,10 @@ async function syncHooks(options = {}) {
|
|
|
6923
7175
|
if (options.dryRun)
|
|
6924
7176
|
return { ...plan, dryRun: true };
|
|
6925
7177
|
if (!apiUrl) {
|
|
6926
|
-
const
|
|
7178
|
+
const db = getDb();
|
|
6927
7179
|
for (const entry of collectBundledCatalog()) {
|
|
6928
7180
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6929
|
-
upsertHookRecord(
|
|
7181
|
+
upsertHookRecord(db, {
|
|
6930
7182
|
name: entry.name,
|
|
6931
7183
|
version: entry.version,
|
|
6932
7184
|
sha256: entry.sha256,
|
|
@@ -6939,13 +7191,17 @@ async function syncHooks(options = {}) {
|
|
|
6939
7191
|
}
|
|
6940
7192
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6941
7193
|
const diff = computeDiff(catalog, remoteLock);
|
|
6942
|
-
const
|
|
6943
|
-
|
|
6944
|
-
|
|
7194
|
+
const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
|
|
7195
|
+
await commitSyncArtifacts(staged, apiUrl, remoteLock);
|
|
7196
|
+
return { apiUrl, dryRun: false, diff };
|
|
7197
|
+
}
|
|
7198
|
+
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
7199
|
+
const staged = [];
|
|
7200
|
+
for (const name of names) {
|
|
6945
7201
|
const entry = remoteLock.hooks[name];
|
|
6946
7202
|
if (!entry)
|
|
6947
7203
|
continue;
|
|
6948
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
7204
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
|
|
6949
7205
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6950
7206
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6951
7207
|
}
|
|
@@ -6953,34 +7209,64 @@ async function syncHooks(options = {}) {
|
|
|
6953
7209
|
if (actualSha !== entry.sha256) {
|
|
6954
7210
|
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6955
7211
|
}
|
|
6956
|
-
artifacts.set(name, artifact);
|
|
6957
|
-
}
|
|
6958
|
-
const db = getDb();
|
|
6959
|
-
for (const name of toFetch) {
|
|
6960
|
-
const artifact = artifacts.get(name);
|
|
6961
|
-
const entry = remoteLock.hooks[name];
|
|
6962
|
-
if (!artifact || !entry)
|
|
6963
|
-
continue;
|
|
6964
7212
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6965
|
-
|
|
6966
|
-
`
|
|
6967
|
-
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6968
|
-
const scriptPath = resolveScriptPath(name);
|
|
6969
|
-
const actual = await sha256File(scriptPath);
|
|
6970
|
-
if (actual !== entry.sha256) {
|
|
6971
|
-
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7213
|
+
if (manifest.version !== entry.version) {
|
|
7214
|
+
throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
|
|
6972
7215
|
}
|
|
6973
|
-
|
|
6974
|
-
|
|
7216
|
+
const scriptRel = scriptRelFor(manifest);
|
|
7217
|
+
staged.push({
|
|
6975
7218
|
name,
|
|
6976
7219
|
version: entry.version,
|
|
6977
7220
|
sha256: entry.sha256,
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
7221
|
+
source: entry.source ?? "remote",
|
|
7222
|
+
manifest,
|
|
7223
|
+
scriptContent: artifact.script,
|
|
7224
|
+
scriptRel
|
|
6981
7225
|
});
|
|
6982
7226
|
}
|
|
6983
|
-
return
|
|
7227
|
+
return staged;
|
|
7228
|
+
}
|
|
7229
|
+
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
7230
|
+
const db = getDb();
|
|
7231
|
+
for (const item of staged) {
|
|
7232
|
+
const entry = remoteLock.hooks[item.name];
|
|
7233
|
+
if (!entry)
|
|
7234
|
+
continue;
|
|
7235
|
+
writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
|
|
7236
|
+
const scriptPath = resolveScriptPath(item.name);
|
|
7237
|
+
const actual = await sha256File(scriptPath);
|
|
7238
|
+
if (actual !== entry.sha256) {
|
|
7239
|
+
throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
const lock = readLock();
|
|
7243
|
+
for (const item of staged) {
|
|
7244
|
+
const entry = remoteLock.hooks[item.name];
|
|
7245
|
+
if (!entry)
|
|
7246
|
+
continue;
|
|
7247
|
+
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
7248
|
+
}
|
|
7249
|
+
writeLock(lock);
|
|
7250
|
+
db.exec("BEGIN");
|
|
7251
|
+
try {
|
|
7252
|
+
for (const item of staged) {
|
|
7253
|
+
const entry = remoteLock.hooks[item.name];
|
|
7254
|
+
if (!entry)
|
|
7255
|
+
continue;
|
|
7256
|
+
upsertHookRecord(db, {
|
|
7257
|
+
name: item.name,
|
|
7258
|
+
version: entry.version,
|
|
7259
|
+
sha256: entry.sha256,
|
|
7260
|
+
source_type: entry.source ?? "remote",
|
|
7261
|
+
source_ref: apiUrl,
|
|
7262
|
+
last_verified_at: new Date().toISOString()
|
|
7263
|
+
});
|
|
7264
|
+
}
|
|
7265
|
+
db.exec("COMMIT");
|
|
7266
|
+
} catch (err) {
|
|
7267
|
+
db.exec("ROLLBACK");
|
|
7268
|
+
throw err;
|
|
7269
|
+
}
|
|
6984
7270
|
}
|
|
6985
7271
|
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6986
7272
|
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
@@ -6988,10 +7274,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6988
7274
|
if (!entry) {
|
|
6989
7275
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
6990
7276
|
}
|
|
6991
|
-
|
|
6992
|
-
|
|
7277
|
+
const isLatest = entry.version === version;
|
|
7278
|
+
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7279
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
6993
7280
|
}
|
|
6994
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}
|
|
7281
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
6995
7282
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6996
7283
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6997
7284
|
}
|
|
@@ -7003,24 +7290,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7003
7290
|
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
7004
7291
|
}
|
|
7005
7292
|
const actualSha = sha256OfText(artifact.script);
|
|
7006
|
-
|
|
7007
|
-
|
|
7293
|
+
let expectedSha = entry.sha256;
|
|
7294
|
+
if (!isLatest) {
|
|
7295
|
+
const headerSha = artifact._headerSha;
|
|
7296
|
+
if (!headerSha) {
|
|
7297
|
+
throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
|
|
7298
|
+
}
|
|
7299
|
+
expectedSha = headerSha;
|
|
7300
|
+
}
|
|
7301
|
+
if (actualSha !== expectedSha) {
|
|
7302
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
|
|
7008
7303
|
}
|
|
7009
7304
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
7010
|
-
const scriptRel =
|
|
7011
|
-
`) ? "script.ts" : artifact.manifest.script;
|
|
7305
|
+
const scriptRel = scriptRelFor(manifest);
|
|
7012
7306
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
7013
7307
|
const scriptPath = resolveScriptPath(name);
|
|
7014
7308
|
const actual = await sha256File(scriptPath);
|
|
7015
|
-
if (actual !==
|
|
7016
|
-
throw new Error(`sha256 mismatch after write for '${name}@${version}':
|
|
7309
|
+
if (actual !== expectedSha) {
|
|
7310
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
7017
7311
|
}
|
|
7018
7312
|
const db = getDb();
|
|
7019
|
-
setPinnedHook(name, { version, sha256:
|
|
7313
|
+
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
7020
7314
|
upsertHookRecord(db, {
|
|
7021
7315
|
name,
|
|
7022
7316
|
version,
|
|
7023
|
-
sha256:
|
|
7317
|
+
sha256: expectedSha,
|
|
7024
7318
|
source_type: entry.source ?? "remote",
|
|
7025
7319
|
source_ref: apiUrl,
|
|
7026
7320
|
last_verified_at: new Date().toISOString()
|
|
@@ -7028,7 +7322,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7028
7322
|
return {
|
|
7029
7323
|
name,
|
|
7030
7324
|
version,
|
|
7031
|
-
sha256:
|
|
7325
|
+
sha256: expectedSha,
|
|
7032
7326
|
source: entry.source ?? "remote",
|
|
7033
7327
|
source_ref: apiUrl,
|
|
7034
7328
|
artifact,
|
|
@@ -7054,6 +7348,7 @@ function secureEqual(a, b) {
|
|
|
7054
7348
|
}
|
|
7055
7349
|
|
|
7056
7350
|
// src/serve.ts
|
|
7351
|
+
init_semver();
|
|
7057
7352
|
var DEFAULT_SERVE_PORT = 39428;
|
|
7058
7353
|
var SERVE_HOST = "127.0.0.1";
|
|
7059
7354
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
@@ -7063,24 +7358,29 @@ async function buildCatalog() {
|
|
|
7063
7358
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
7064
7359
|
if (!scriptPath)
|
|
7065
7360
|
continue;
|
|
7361
|
+
const sha = await sha256File(scriptPath);
|
|
7066
7362
|
byName.set(meta.name, {
|
|
7067
7363
|
name: meta.name,
|
|
7068
7364
|
version: meta.version,
|
|
7069
|
-
sha256:
|
|
7365
|
+
sha256: sha,
|
|
7070
7366
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
7071
7367
|
description: meta.description,
|
|
7072
|
-
source: "bundled"
|
|
7368
|
+
source: "bundled",
|
|
7369
|
+
versions: [meta.version]
|
|
7073
7370
|
});
|
|
7074
7371
|
}
|
|
7075
7372
|
for (const custom2 of listCustomHooks()) {
|
|
7076
7373
|
const name = shortManifestName(custom2.manifest.name);
|
|
7374
|
+
const sha = await sha256File(custom2.scriptPath);
|
|
7375
|
+
const existing = byName.get(name);
|
|
7077
7376
|
byName.set(name, {
|
|
7078
7377
|
name,
|
|
7079
7378
|
version: custom2.manifest.version,
|
|
7080
|
-
sha256:
|
|
7379
|
+
sha256: sha,
|
|
7081
7380
|
events: custom2.manifest.events,
|
|
7082
7381
|
description: custom2.manifest.description ?? "Custom hook",
|
|
7083
|
-
source:
|
|
7382
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
7383
|
+
versions: existing ? [...new Set([...existing.versions, custom2.manifest.version])] : [custom2.manifest.version]
|
|
7084
7384
|
});
|
|
7085
7385
|
}
|
|
7086
7386
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -7098,6 +7398,7 @@ async function artifactFor(name, version) {
|
|
|
7098
7398
|
description: resolved.description,
|
|
7099
7399
|
events: resolved.events,
|
|
7100
7400
|
script: custom2 ? custom2.manifest.script : `src/hook.ts`,
|
|
7401
|
+
script_kind: custom2?.manifest.script_kind,
|
|
7101
7402
|
args: custom2?.manifest.args,
|
|
7102
7403
|
timeout_ms: custom2?.manifest.timeout_ms
|
|
7103
7404
|
};
|
|
@@ -7127,11 +7428,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
7127
7428
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
7128
7429
|
}
|
|
7129
7430
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
7130
|
-
return
|
|
7431
|
+
return buildCatalog().then((catalog) => {
|
|
7432
|
+
const lock = readLock();
|
|
7433
|
+
const byVersion = {};
|
|
7434
|
+
for (const entry of catalog)
|
|
7435
|
+
byVersion[entry.name] = entry.versions;
|
|
7436
|
+
const hooks = {};
|
|
7437
|
+
for (const [name, pin] of Object.entries(lock.hooks)) {
|
|
7438
|
+
hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
|
|
7439
|
+
}
|
|
7440
|
+
return json({ hooks });
|
|
7441
|
+
});
|
|
7131
7442
|
}
|
|
7132
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
7443
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
7133
7444
|
if (artifactMatch && req.method === "GET") {
|
|
7134
|
-
const [,
|
|
7445
|
+
const [, rawName, rawVersion] = artifactMatch;
|
|
7446
|
+
let name;
|
|
7447
|
+
let version;
|
|
7448
|
+
try {
|
|
7449
|
+
name = decodeURIComponent(rawName);
|
|
7450
|
+
version = decodeURIComponent(rawVersion);
|
|
7451
|
+
} catch {
|
|
7452
|
+
return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
|
|
7453
|
+
}
|
|
7454
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
7455
|
+
return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
|
|
7456
|
+
}
|
|
7135
7457
|
return artifactFor(name, version).then(async (artifact) => {
|
|
7136
7458
|
if (!artifact)
|
|
7137
7459
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -7162,7 +7484,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
7162
7484
|
function startServeServer(options) {
|
|
7163
7485
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
7164
7486
|
const host = options.host ?? SERVE_HOST;
|
|
7165
|
-
const apiKey = resolveApiKey(
|
|
7487
|
+
const apiKey = resolveApiKey();
|
|
7166
7488
|
const server = Bun.serve({
|
|
7167
7489
|
hostname: host,
|
|
7168
7490
|
port,
|
|
@@ -7308,7 +7630,6 @@ async function runHook(name, input, options = {}) {
|
|
|
7308
7630
|
content,
|
|
7309
7631
|
args: custom2?.manifest.args ?? [],
|
|
7310
7632
|
stdin: JSON.stringify(hookInput),
|
|
7311
|
-
env: process.env,
|
|
7312
7633
|
timeout: effectiveTimeout ?? undefined
|
|
7313
7634
|
}));
|
|
7314
7635
|
} catch (err) {
|