@hasna/hooks 0.9.5 → 0.9.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 +27 -0
- package/bin/hooks-mcp.js +282 -123
- package/bin/index.js +625 -252
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +329 -127
- package/dist/index.js +610 -249
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +14 -15
- package/dist/sdk/index.js +172 -1
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +7 -4
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +34 -0
- package/scripts/verify-generated-artifacts.ts +237 -0
package/dist/index.js
CHANGED
|
@@ -4762,12 +4762,12 @@ __export(exports_store, {
|
|
|
4762
4762
|
checkScriptHash: () => checkScriptHash,
|
|
4763
4763
|
LockFileError: () => LockFileError
|
|
4764
4764
|
});
|
|
4765
|
-
import { createHash } from "crypto";
|
|
4766
|
-
import { existsSync as
|
|
4765
|
+
import { createHash as createHash2 } from "crypto";
|
|
4766
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
4767
4767
|
import { readFile } from "fs/promises";
|
|
4768
4768
|
import { randomBytes } from "crypto";
|
|
4769
4769
|
function sha256Of(input) {
|
|
4770
|
-
return
|
|
4770
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
4771
4771
|
}
|
|
4772
4772
|
async function sha256File(path) {
|
|
4773
4773
|
const buf = await readFile(path);
|
|
@@ -4828,11 +4828,11 @@ function assertLockShape(parsed, path) {
|
|
|
4828
4828
|
}
|
|
4829
4829
|
function readLock() {
|
|
4830
4830
|
const path = getLockPath();
|
|
4831
|
-
if (!
|
|
4831
|
+
if (!existsSync7(path))
|
|
4832
4832
|
return { hooks: {} };
|
|
4833
4833
|
let raw;
|
|
4834
4834
|
try {
|
|
4835
|
-
raw =
|
|
4835
|
+
raw = readFileSync4(path, "utf-8");
|
|
4836
4836
|
} catch {
|
|
4837
4837
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4838
4838
|
}
|
|
@@ -4935,7 +4935,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
4935
4935
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
4936
4936
|
}
|
|
4937
4937
|
function retrustHook(name, scriptPath, version, source) {
|
|
4938
|
-
const actual = sha256Of(
|
|
4938
|
+
const actual = sha256Of(readFileSync4(scriptPath));
|
|
4939
4939
|
const db = storeDb();
|
|
4940
4940
|
if (db) {
|
|
4941
4941
|
upsertHookRecord(db, {
|
|
@@ -4974,12 +4974,12 @@ __export(exports_profiles, {
|
|
|
4974
4974
|
deleteProfile: () => deleteProfile,
|
|
4975
4975
|
createProfile: () => createProfile
|
|
4976
4976
|
});
|
|
4977
|
-
import { existsSync as
|
|
4978
|
-
import { join as
|
|
4977
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
4978
|
+
import { join as join9 } from "path";
|
|
4979
4979
|
function getProfilesDir() {
|
|
4980
|
-
const newDir =
|
|
4981
|
-
const oldDir =
|
|
4982
|
-
if (!
|
|
4980
|
+
const newDir = join9(getEffectiveDataRoot(), "profiles");
|
|
4981
|
+
const oldDir = join9(getHomeDir(), ".hooks", "profiles");
|
|
4982
|
+
if (!existsSync10(newDir) && existsSync10(oldDir)) {
|
|
4983
4983
|
mkdirSync6(newDir, { recursive: true });
|
|
4984
4984
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
4985
4985
|
}
|
|
@@ -4987,12 +4987,12 @@ function getProfilesDir() {
|
|
|
4987
4987
|
}
|
|
4988
4988
|
function ensureProfilesDir() {
|
|
4989
4989
|
const dir = getProfilesDir();
|
|
4990
|
-
if (!
|
|
4990
|
+
if (!existsSync10(dir)) {
|
|
4991
4991
|
mkdirSync6(dir, { recursive: true });
|
|
4992
4992
|
}
|
|
4993
4993
|
}
|
|
4994
4994
|
function profilePath(id) {
|
|
4995
|
-
return
|
|
4995
|
+
return join9(getProfilesDir(), `${id}.json`);
|
|
4996
4996
|
}
|
|
4997
4997
|
function shortUuid() {
|
|
4998
4998
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -5018,23 +5018,23 @@ function createProfile(input) {
|
|
|
5018
5018
|
function getProfile(id) {
|
|
5019
5019
|
const path = profilePath(id);
|
|
5020
5020
|
try {
|
|
5021
|
-
if (!
|
|
5021
|
+
if (!existsSync10(path))
|
|
5022
5022
|
return null;
|
|
5023
|
-
return JSON.parse(
|
|
5023
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
5024
5024
|
} catch {
|
|
5025
5025
|
return null;
|
|
5026
5026
|
}
|
|
5027
5027
|
}
|
|
5028
5028
|
function listProfiles() {
|
|
5029
5029
|
const dir = getProfilesDir();
|
|
5030
|
-
if (!
|
|
5030
|
+
if (!existsSync10(dir))
|
|
5031
5031
|
return [];
|
|
5032
5032
|
try {
|
|
5033
5033
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
5034
5034
|
const profiles = [];
|
|
5035
5035
|
for (const file of files) {
|
|
5036
5036
|
try {
|
|
5037
|
-
const content =
|
|
5037
|
+
const content = readFileSync7(join9(dir, file), "utf-8");
|
|
5038
5038
|
profiles.push(JSON.parse(content));
|
|
5039
5039
|
} catch {}
|
|
5040
5040
|
}
|
|
@@ -5057,7 +5057,7 @@ function updateProfile(id, data) {
|
|
|
5057
5057
|
}
|
|
5058
5058
|
function deleteProfile(id) {
|
|
5059
5059
|
const path = profilePath(id);
|
|
5060
|
-
if (!
|
|
5060
|
+
if (!existsSync10(path))
|
|
5061
5061
|
return false;
|
|
5062
5062
|
try {
|
|
5063
5063
|
rmSync(path);
|
|
@@ -5087,7 +5087,7 @@ function importProfiles(profiles) {
|
|
|
5087
5087
|
continue;
|
|
5088
5088
|
}
|
|
5089
5089
|
const path = profilePath(profile.agent_id);
|
|
5090
|
-
if (
|
|
5090
|
+
if (existsSync10(path)) {
|
|
5091
5091
|
skipped++;
|
|
5092
5092
|
continue;
|
|
5093
5093
|
}
|
|
@@ -5104,15 +5104,15 @@ var init_profiles = __esm(() => {
|
|
|
5104
5104
|
// ../contracts/dist/client/transport.js
|
|
5105
5105
|
import { isIP } from "net";
|
|
5106
5106
|
import { spawnSync } from "child_process";
|
|
5107
|
-
import { closeSync as
|
|
5107
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
5108
5108
|
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
5109
5109
|
import { hostname as osHostname } from "os";
|
|
5110
|
-
import { isAbsolute as
|
|
5110
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
5111
5111
|
import { createRequire } from "module";
|
|
5112
5112
|
import { fileURLToPath as fileURLToPath22 } from "url";
|
|
5113
|
-
import { closeSync as closeSync3, fstatSync, lstatSync as
|
|
5113
|
+
import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
|
|
5114
5114
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5115
|
-
import { basename as basename2, dirname as
|
|
5115
|
+
import { basename as basename2, dirname as dirname6, join as join10, relative, sep as sep2 } from "path";
|
|
5116
5116
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
5117
5117
|
function envToken(name) {
|
|
5118
5118
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -5186,14 +5186,14 @@ function o(e2, r2, t2) {
|
|
|
5186
5186
|
}
|
|
5187
5187
|
}
|
|
5188
5188
|
function entryPoint(directory) {
|
|
5189
|
-
const root =
|
|
5190
|
-
const fd = openSync3(
|
|
5189
|
+
const root = realpathSync3(directory);
|
|
5190
|
+
const fd = openSync3(join10(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
5191
5191
|
let pkg;
|
|
5192
5192
|
try {
|
|
5193
|
-
const stat =
|
|
5193
|
+
const stat = fstatSync3(fd);
|
|
5194
5194
|
if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
|
|
5195
5195
|
throw new Error("Invalid Secrets package metadata");
|
|
5196
|
-
pkg = JSON.parse(
|
|
5196
|
+
pkg = JSON.parse(readFileSync8(fd, "utf8"));
|
|
5197
5197
|
} finally {
|
|
5198
5198
|
closeSync3(fd);
|
|
5199
5199
|
}
|
|
@@ -5207,7 +5207,7 @@ function entryPoint(directory) {
|
|
|
5207
5207
|
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
5208
5208
|
throw new Error("Invalid Secrets import export");
|
|
5209
5209
|
}
|
|
5210
|
-
const file =
|
|
5210
|
+
const file = realpathSync3(join10(root, ...parts));
|
|
5211
5211
|
const within = relative(root, file);
|
|
5212
5212
|
if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
|
|
5213
5213
|
throw new Error("Secrets import export escapes its package or is not a file");
|
|
@@ -5215,13 +5215,13 @@ function entryPoint(directory) {
|
|
|
5215
5215
|
return pathToFileURL(file).href;
|
|
5216
5216
|
}
|
|
5217
5217
|
function resolveInstalledSecrets(parentUrl) {
|
|
5218
|
-
let directory =
|
|
5218
|
+
let directory = dirname6(fileURLToPath3(parentUrl));
|
|
5219
5219
|
for (;; ) {
|
|
5220
5220
|
if (basename2(directory) !== "node_modules") {
|
|
5221
|
-
const candidate =
|
|
5221
|
+
const candidate = join10(directory, "node_modules", PACKAGE);
|
|
5222
5222
|
let absent = false;
|
|
5223
5223
|
try {
|
|
5224
|
-
|
|
5224
|
+
lstatSync3(candidate);
|
|
5225
5225
|
} catch (error) {
|
|
5226
5226
|
if (error.code !== "ENOENT")
|
|
5227
5227
|
throw error;
|
|
@@ -5230,7 +5230,7 @@ function resolveInstalledSecrets(parentUrl) {
|
|
|
5230
5230
|
if (!absent)
|
|
5231
5231
|
return entryPoint(candidate);
|
|
5232
5232
|
}
|
|
5233
|
-
const parent =
|
|
5233
|
+
const parent = dirname6(directory);
|
|
5234
5234
|
if (parent === directory)
|
|
5235
5235
|
throw new Error("Secrets SDK is not installed for this consumer");
|
|
5236
5236
|
directory = parent;
|
|
@@ -5242,7 +5242,7 @@ function homeDir(env) {
|
|
|
5242
5242
|
}
|
|
5243
5243
|
function absoluteOverride(env, key) {
|
|
5244
5244
|
const value = env[key]?.trim();
|
|
5245
|
-
return value &&
|
|
5245
|
+
return value && isAbsolute3(value) ? value : null;
|
|
5246
5246
|
}
|
|
5247
5247
|
function hasnaHomeDir(env) {
|
|
5248
5248
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
@@ -5288,9 +5288,9 @@ function parseEnvFile(text) {
|
|
|
5288
5288
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5289
5289
|
continue;
|
|
5290
5290
|
let value = withoutExport.slice(equals + 1).trim();
|
|
5291
|
-
const
|
|
5292
|
-
if (
|
|
5293
|
-
if (value.length < 2 || !value.endsWith(
|
|
5291
|
+
const quote2 = value[0];
|
|
5292
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
5293
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
5294
5294
|
unusable.add(key);
|
|
5295
5295
|
continue;
|
|
5296
5296
|
}
|
|
@@ -5319,7 +5319,7 @@ function readAppConfigFile(path) {
|
|
|
5319
5319
|
};
|
|
5320
5320
|
let fd = -1;
|
|
5321
5321
|
try {
|
|
5322
|
-
fd =
|
|
5322
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
5323
5323
|
} catch (error) {
|
|
5324
5324
|
const code = error.code;
|
|
5325
5325
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -5348,7 +5348,7 @@ function readAppConfigFile(path) {
|
|
|
5348
5348
|
return parseEnvFile(bytes.toString("utf8"));
|
|
5349
5349
|
} finally {
|
|
5350
5350
|
if (fd !== -1)
|
|
5351
|
-
|
|
5351
|
+
closeSync22(fd);
|
|
5352
5352
|
}
|
|
5353
5353
|
}
|
|
5354
5354
|
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
@@ -6202,12 +6202,12 @@ var init_db_writer = __esm(() => {
|
|
|
6202
6202
|
});
|
|
6203
6203
|
|
|
6204
6204
|
// src/lib/hook-env.ts
|
|
6205
|
-
import { dirname as
|
|
6206
|
-
import { existsSync as
|
|
6205
|
+
import { dirname as dirname8, isAbsolute as isAbsolute4 } from "path";
|
|
6206
|
+
import { existsSync as existsSync12, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
|
|
6207
6207
|
import { tmpdir as tmpdir2 } from "os";
|
|
6208
6208
|
function isDeniedEnvName(name) {
|
|
6209
6209
|
const upper = name.toUpperCase();
|
|
6210
|
-
if (DENY_PREFIXES.some((
|
|
6210
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
6211
6211
|
return true;
|
|
6212
6212
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
6213
6213
|
return true;
|
|
@@ -6216,13 +6216,13 @@ function isDeniedEnvName(name) {
|
|
|
6216
6216
|
function isInterpreterInjectionEnvName(name) {
|
|
6217
6217
|
if (INTERPRETER_INJECTION.has(name))
|
|
6218
6218
|
return true;
|
|
6219
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
6219
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
6220
6220
|
}
|
|
6221
6221
|
function isBashFunctionEnvName(name) {
|
|
6222
6222
|
return name.startsWith("BASH_FUNC_");
|
|
6223
6223
|
}
|
|
6224
6224
|
function isUnsafePathEntry(entry, home) {
|
|
6225
|
-
if (entry === "" || !
|
|
6225
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
6226
6226
|
return true;
|
|
6227
6227
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
6228
6228
|
return true;
|
|
@@ -6232,7 +6232,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6232
6232
|
return true;
|
|
6233
6233
|
const canonical = (path) => {
|
|
6234
6234
|
try {
|
|
6235
|
-
return
|
|
6235
|
+
return realpathSync4(path);
|
|
6236
6236
|
} catch {
|
|
6237
6237
|
return path;
|
|
6238
6238
|
}
|
|
@@ -6251,9 +6251,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6251
6251
|
}
|
|
6252
6252
|
function sanitizeHookPath(pathValue, home) {
|
|
6253
6253
|
const baseline = PATH_BASELINE.slice();
|
|
6254
|
-
if (process.platform === "darwin" &&
|
|
6254
|
+
if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
|
|
6255
6255
|
baseline.push("/opt/homebrew/bin");
|
|
6256
|
-
const bunDir =
|
|
6256
|
+
const bunDir = dirname8(process.execPath);
|
|
6257
6257
|
const seen = new Set;
|
|
6258
6258
|
const out = [];
|
|
6259
6259
|
for (const dir of baseline) {
|
|
@@ -6378,7 +6378,7 @@ __export(exports_run, {
|
|
|
6378
6378
|
});
|
|
6379
6379
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6380
6380
|
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6381
|
-
import { basename as basename3, dirname as
|
|
6381
|
+
import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute5, join as join13 } from "path";
|
|
6382
6382
|
function extensionOf(scriptPath) {
|
|
6383
6383
|
const base = basename3(scriptPath);
|
|
6384
6384
|
const dot = base.lastIndexOf(".");
|
|
@@ -6447,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6447
6447
|
return chunks.join("");
|
|
6448
6448
|
}
|
|
6449
6449
|
async function executeVerifiedScript(options) {
|
|
6450
|
-
const scriptDir =
|
|
6450
|
+
const scriptDir = dirname9(options.scriptPath);
|
|
6451
6451
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6452
|
-
const tempPath =
|
|
6452
|
+
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6453
6453
|
let proc = null;
|
|
6454
6454
|
let timedOut = false;
|
|
6455
6455
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6458,7 +6458,7 @@ async function executeVerifiedScript(options) {
|
|
|
6458
6458
|
}, options.timeout) : null;
|
|
6459
6459
|
try {
|
|
6460
6460
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
6461
|
-
proc = Bun.spawn([...interpreter.command,
|
|
6461
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
6462
6462
|
stdin: new Response(options.stdin),
|
|
6463
6463
|
stdout: "pipe",
|
|
6464
6464
|
stderr: "pipe",
|
|
@@ -7034,6 +7034,7 @@ var HOOKS = [
|
|
|
7034
7034
|
},
|
|
7035
7035
|
{
|
|
7036
7036
|
name: "workspace-repos-guard",
|
|
7037
|
+
timeoutSeconds: 10,
|
|
7037
7038
|
displayName: "Workspace Repos Guard",
|
|
7038
7039
|
description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
|
|
7039
7040
|
version: "0.1.0",
|
|
@@ -7052,7 +7053,7 @@ var HOOKS = [
|
|
|
7052
7053
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
7053
7054
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
7054
7055
|
rewritesInput: true,
|
|
7055
|
-
timeoutSeconds:
|
|
7056
|
+
timeoutSeconds: 10
|
|
7056
7057
|
},
|
|
7057
7058
|
{
|
|
7058
7059
|
name: "checktests",
|
|
@@ -7541,9 +7542,9 @@ function getHook(name) {
|
|
|
7541
7542
|
return HOOKS.find((h) => h.name === name);
|
|
7542
7543
|
}
|
|
7543
7544
|
// src/lib/installer.ts
|
|
7544
|
-
import { existsSync as
|
|
7545
|
-
import { join as
|
|
7546
|
-
import { homedir as
|
|
7545
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
7546
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
7547
|
+
import { homedir as homedir4 } from "os";
|
|
7547
7548
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7548
7549
|
|
|
7549
7550
|
// src/lib/resolve.ts
|
|
@@ -7634,6 +7635,137 @@ function resolveHook(name) {
|
|
|
7634
7635
|
// src/lib/installer.ts
|
|
7635
7636
|
init_manifest();
|
|
7636
7637
|
|
|
7638
|
+
// src/lib/native-safety.ts
|
|
7639
|
+
import { createHash } from "crypto";
|
|
7640
|
+
import { closeSync, constants, existsSync as existsSync6, fstatSync, lstatSync, openSync, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
|
|
7641
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
7642
|
+
import { homedir as homedir3 } from "os";
|
|
7643
|
+
function isNativeSafetyName(name) {
|
|
7644
|
+
return name === "trash-guard" || name === "workspace-repos-guard";
|
|
7645
|
+
}
|
|
7646
|
+
var supervisorSource = String.raw`async function supervise(encoded) {
|
|
7647
|
+
const fs = require("node:fs");
|
|
7648
|
+
const path = require("node:path");
|
|
7649
|
+
const crypto = require("node:crypto");
|
|
7650
|
+
const cp = require("node:child_process");
|
|
7651
|
+
const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
|
|
7652
|
+
try {
|
|
7653
|
+
const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
7654
|
+
const check = (file, hash, maximum) => {
|
|
7655
|
+
if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
|
|
7656
|
+
for (let parent = path.dirname(file);;) {
|
|
7657
|
+
const s = fs.lstatSync(parent);
|
|
7658
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
|
|
7659
|
+
if (parent === path.dirname(parent)) break;
|
|
7660
|
+
parent = path.dirname(parent);
|
|
7661
|
+
}
|
|
7662
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
7663
|
+
try {
|
|
7664
|
+
const s = fs.fstatSync(fd);
|
|
7665
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
|
|
7666
|
+
const bytes = fs.readFileSync(fd);
|
|
7667
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
|
|
7668
|
+
return bytes;
|
|
7669
|
+
} finally { fs.closeSync(fd); }
|
|
7670
|
+
};
|
|
7671
|
+
check(b.runtime, b.runtimeSHA256, 200_000_000);
|
|
7672
|
+
const bytes = check(b.worker, b.workerSHA256, 256_000);
|
|
7673
|
+
const deadline = setTimeout(fail, 1500);
|
|
7674
|
+
const chunks = []; let size = 0;
|
|
7675
|
+
for await (const chunk of process.stdin) {
|
|
7676
|
+
size += chunk.length;
|
|
7677
|
+
if (size > 1_048_576) throw Error();
|
|
7678
|
+
chunks.push(chunk);
|
|
7679
|
+
}
|
|
7680
|
+
clearTimeout(deadline);
|
|
7681
|
+
const input = Buffer.concat(chunks);
|
|
7682
|
+
const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
|
|
7683
|
+
input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
|
|
7684
|
+
});
|
|
7685
|
+
if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
|
|
7686
|
+
const result = JSON.parse(child.stdout.toString("utf8"));
|
|
7687
|
+
if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
|
|
7688
|
+
if (result.verdict === null) return;
|
|
7689
|
+
const output = result.verdict?.hookSpecificOutput;
|
|
7690
|
+
if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
|
|
7691
|
+
if (output.permissionDecision === "deny") {
|
|
7692
|
+
if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
|
|
7693
|
+
} else if (output.permissionDecision === "allow") {
|
|
7694
|
+
if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
|
|
7695
|
+
} else throw Error();
|
|
7696
|
+
process.stdout.write(JSON.stringify(result.verdict));
|
|
7697
|
+
} catch { fail(); }
|
|
7698
|
+
}`;
|
|
7699
|
+
var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
7700
|
+
var program = `(${supervisorSource})(process.argv[1]);`;
|
|
7701
|
+
var shell = `cd / || exit 2
|
|
7702
|
+
exec 3<&0 || exit 2
|
|
7703
|
+
/usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
|
|
7704
|
+
runner=$!
|
|
7705
|
+
exec 3<&-
|
|
7706
|
+
(trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
|
|
7707
|
+
watchdog=$!
|
|
7708
|
+
wait "$runner"; result=$?
|
|
7709
|
+
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
|
|
7710
|
+
if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
|
|
7711
|
+
var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
|
|
7712
|
+
function validateBinding(b) {
|
|
7713
|
+
if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
|
|
7714
|
+
throw new Error("Invalid native safety binding");
|
|
7715
|
+
}
|
|
7716
|
+
function buildNativeSafetyCommand(binding) {
|
|
7717
|
+
validateBinding(binding);
|
|
7718
|
+
const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
|
|
7719
|
+
return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
|
|
7720
|
+
}
|
|
7721
|
+
function nativeSafetyRegistration(command) {
|
|
7722
|
+
if (!command.startsWith(prefix))
|
|
7723
|
+
return;
|
|
7724
|
+
const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
|
|
7725
|
+
if (!encoded || encoded.length > 16384)
|
|
7726
|
+
return;
|
|
7727
|
+
try {
|
|
7728
|
+
const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
7729
|
+
return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
|
|
7730
|
+
} catch {
|
|
7731
|
+
return;
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
function pinEligibleFile(file, maximum, executable = false) {
|
|
7735
|
+
if (!isAbsolute2(file) || realpathSync2(file) !== file)
|
|
7736
|
+
throw new Error("Native safety requires canonical files");
|
|
7737
|
+
for (let parent = dirname3(file);; ) {
|
|
7738
|
+
const s = lstatSync(parent);
|
|
7739
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
|
|
7740
|
+
throw new Error("Native safety requires trusted, non-writable file ancestry");
|
|
7741
|
+
if (parent === dirname3(parent))
|
|
7742
|
+
break;
|
|
7743
|
+
parent = dirname3(parent);
|
|
7744
|
+
}
|
|
7745
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7746
|
+
try {
|
|
7747
|
+
const s = fstatSync(fd);
|
|
7748
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
|
|
7749
|
+
throw new Error("Native safety requires an eligible installed file");
|
|
7750
|
+
const bytes = readFileSync3(fd);
|
|
7751
|
+
if (bytes.length > maximum)
|
|
7752
|
+
throw new Error("Native safety file exceeds its bound");
|
|
7753
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
7754
|
+
} finally {
|
|
7755
|
+
closeSync(fd);
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
function installedNativeSafetyCommand(name) {
|
|
7759
|
+
const home = realpathSync2(homedir3());
|
|
7760
|
+
const runtime = realpathSync2(process.execPath);
|
|
7761
|
+
const candidates = [join7(import.meta.dir, "../../bin/native-safety-entry.js"), join7(import.meta.dir, "../bin/native-safety-entry.js"), join7(import.meta.dir, "native-safety-entry.js")];
|
|
7762
|
+
const worker = candidates.find(existsSync6);
|
|
7763
|
+
if (!worker)
|
|
7764
|
+
throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
|
|
7765
|
+
const canonicalWorker = realpathSync2(worker);
|
|
7766
|
+
return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join7(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
|
|
7767
|
+
}
|
|
7768
|
+
|
|
7637
7769
|
// src/lib/mementos-options.ts
|
|
7638
7770
|
function validateMementosRegistration(name, value) {
|
|
7639
7771
|
if (value === undefined)
|
|
@@ -7695,6 +7827,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
|
|
|
7695
7827
|
function registrationName(command) {
|
|
7696
7828
|
if (typeof command !== "string")
|
|
7697
7829
|
return;
|
|
7830
|
+
const native = nativeSafetyRegistration(command);
|
|
7831
|
+
if (native)
|
|
7832
|
+
return native;
|
|
7698
7833
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
7699
7834
|
try {
|
|
7700
7835
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -7714,16 +7849,16 @@ function matchersOverlap(a, b) {
|
|
|
7714
7849
|
init_store();
|
|
7715
7850
|
|
|
7716
7851
|
// src/lib/codex-settings.ts
|
|
7717
|
-
import { closeSync, existsSync as
|
|
7718
|
-
import { dirname as
|
|
7852
|
+
import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
7853
|
+
import { dirname as dirname4 } from "path";
|
|
7719
7854
|
import { randomUUID } from "crypto";
|
|
7720
7855
|
var snapshots = new WeakMap;
|
|
7721
7856
|
function current(path) {
|
|
7722
7857
|
try {
|
|
7723
|
-
const stat =
|
|
7858
|
+
const stat = lstatSync2(path);
|
|
7724
7859
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
7725
7860
|
throw new Error("invalid_file");
|
|
7726
|
-
return
|
|
7861
|
+
return readFileSync5(path, "utf8");
|
|
7727
7862
|
} catch (error) {
|
|
7728
7863
|
if (error.code === "ENOENT")
|
|
7729
7864
|
return null;
|
|
@@ -7755,11 +7890,11 @@ function writeCodexSettings(path, settings) {
|
|
|
7755
7890
|
const snapshot = snapshots.get(settings);
|
|
7756
7891
|
if (!snapshot || snapshot.path !== path)
|
|
7757
7892
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
7758
|
-
mkdirSync4(
|
|
7893
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
7759
7894
|
const lock = `${path}.hooks-write.lock`;
|
|
7760
7895
|
let fd;
|
|
7761
7896
|
try {
|
|
7762
|
-
fd =
|
|
7897
|
+
fd = openSync2(lock, "wx", 384);
|
|
7763
7898
|
} catch {
|
|
7764
7899
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
7765
7900
|
}
|
|
@@ -7775,16 +7910,16 @@ function writeCodexSettings(path, settings) {
|
|
|
7775
7910
|
renameSync2(temp, path);
|
|
7776
7911
|
snapshots.set(settings, { path, raw: next });
|
|
7777
7912
|
} finally {
|
|
7778
|
-
if (
|
|
7913
|
+
if (existsSync8(temp))
|
|
7779
7914
|
unlinkSync(temp);
|
|
7780
|
-
|
|
7915
|
+
closeSync2(fd);
|
|
7781
7916
|
unlinkSync(lock);
|
|
7782
7917
|
}
|
|
7783
7918
|
}
|
|
7784
7919
|
|
|
7785
7920
|
// src/lib/installer.ts
|
|
7786
|
-
var __dirname2 =
|
|
7787
|
-
var HOOKS_DIR =
|
|
7921
|
+
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
7922
|
+
var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
|
|
7788
7923
|
function normalizeHookName(name) {
|
|
7789
7924
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
7790
7925
|
}
|
|
@@ -7796,8 +7931,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
7796
7931
|
if (!Array.isArray(entry.hooks))
|
|
7797
7932
|
return [entry];
|
|
7798
7933
|
const kept = entry.hooks.filter((h) => {
|
|
7799
|
-
|
|
7800
|
-
return !(match && match[1] === hookName);
|
|
7934
|
+
return registrationName(h.command) !== hookName;
|
|
7801
7935
|
});
|
|
7802
7936
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
7803
7937
|
});
|
|
@@ -7873,29 +8007,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
7873
8007
|
return globalOverride;
|
|
7874
8008
|
const dir = getTargetSettingsDir(target);
|
|
7875
8009
|
if (target === "codex")
|
|
7876
|
-
return
|
|
8010
|
+
return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
7877
8011
|
if (scope === "project") {
|
|
7878
|
-
return target === "codewith" ?
|
|
8012
|
+
return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
|
|
7879
8013
|
}
|
|
7880
|
-
return target === "codewith" ?
|
|
8014
|
+
return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
|
|
7881
8015
|
}
|
|
7882
8016
|
function getHookPath(name) {
|
|
7883
8017
|
const shortName = shortHookName(name);
|
|
7884
8018
|
const resolved = resolveHookDir(shortName);
|
|
7885
8019
|
if (resolved)
|
|
7886
8020
|
return resolved;
|
|
7887
|
-
return
|
|
8021
|
+
return join8(HOOKS_DIR, normalizeHookName(shortName));
|
|
7888
8022
|
}
|
|
7889
8023
|
function hookExists(name) {
|
|
7890
|
-
return
|
|
8024
|
+
return existsSync9(getHookPath(name));
|
|
7891
8025
|
}
|
|
7892
8026
|
function readSettings(scope = "global", target = "claude") {
|
|
7893
8027
|
const path = getSettingsPath(scope, target);
|
|
7894
8028
|
if (target === "codex")
|
|
7895
8029
|
return readCodexSettings(path);
|
|
7896
8030
|
try {
|
|
7897
|
-
if (
|
|
7898
|
-
return JSON.parse(
|
|
8031
|
+
if (existsSync9(path)) {
|
|
8032
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
7899
8033
|
}
|
|
7900
8034
|
} catch (error) {
|
|
7901
8035
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -7906,8 +8040,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
7906
8040
|
const path = getSettingsPath(scope, target);
|
|
7907
8041
|
if (target === "codex")
|
|
7908
8042
|
return writeCodexSettings(path, settings);
|
|
7909
|
-
const dir =
|
|
7910
|
-
if (!
|
|
8043
|
+
const dir = dirname5(path);
|
|
8044
|
+
if (!existsSync9(dir)) {
|
|
7911
8045
|
mkdirSync5(dir, { recursive: true });
|
|
7912
8046
|
}
|
|
7913
8047
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -7935,6 +8069,9 @@ function codewithTimeout(name) {
|
|
|
7935
8069
|
switch (name) {
|
|
7936
8070
|
case "mementos-context":
|
|
7937
8071
|
return 12;
|
|
8072
|
+
case "trash-guard":
|
|
8073
|
+
case "workspace-repos-guard":
|
|
8074
|
+
return 10;
|
|
7938
8075
|
case "knowledge-context":
|
|
7939
8076
|
return 6;
|
|
7940
8077
|
case "session-start":
|
|
@@ -7944,7 +8081,6 @@ function codewithTimeout(name) {
|
|
|
7944
8081
|
case "prompt-guard":
|
|
7945
8082
|
return 3;
|
|
7946
8083
|
case "worktree-guard":
|
|
7947
|
-
case "trash-guard":
|
|
7948
8084
|
case "stop-sync":
|
|
7949
8085
|
return 5;
|
|
7950
8086
|
default:
|
|
@@ -7973,11 +8109,13 @@ function codewithStatusMessage(name) {
|
|
|
7973
8109
|
}
|
|
7974
8110
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
7975
8111
|
const shortName = shortHookName(name);
|
|
7976
|
-
|
|
8112
|
+
if (isNativeSafetyName(shortName) && profile)
|
|
8113
|
+
throw new Error("Bundled native safety guards do not select registry profiles");
|
|
8114
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
7977
8115
|
if (!meta)
|
|
7978
8116
|
throw new Error(`Hook '${shortName}' not found`);
|
|
7979
8117
|
validateMementosRegistration(shortName, mementos);
|
|
7980
|
-
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
8118
|
+
const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
7981
8119
|
const matcher = codewithMatcher(meta.matcher);
|
|
7982
8120
|
const fragments = [];
|
|
7983
8121
|
for (const event of getHookEvents(meta)) {
|
|
@@ -8002,17 +8140,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
8002
8140
|
function readCodewithConfig(scope, configPath) {
|
|
8003
8141
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
8004
8142
|
try {
|
|
8005
|
-
return
|
|
8143
|
+
return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
8006
8144
|
} catch {
|
|
8007
8145
|
return "";
|
|
8008
8146
|
}
|
|
8009
8147
|
}
|
|
8010
8148
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
8011
8149
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
8012
|
-
const dir =
|
|
8013
|
-
if (!
|
|
8150
|
+
const dir = dirname5(path);
|
|
8151
|
+
if (!existsSync9(dir))
|
|
8014
8152
|
mkdirSync5(dir, { recursive: true });
|
|
8015
|
-
const existing =
|
|
8153
|
+
const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
8016
8154
|
const sep2 = existing.trim() ? `
|
|
8017
8155
|
|
|
8018
8156
|
` : "";
|
|
@@ -8020,7 +8158,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
8020
8158
|
return path;
|
|
8021
8159
|
}
|
|
8022
8160
|
function detectConflict(name, scope, target) {
|
|
8023
|
-
const meta = resolveHookMeta(name);
|
|
8161
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
8024
8162
|
if (!meta || !meta.matcher)
|
|
8025
8163
|
return;
|
|
8026
8164
|
const events = new Set(getHookEvents(meta));
|
|
@@ -8040,7 +8178,7 @@ function detectConflict(name, scope, target) {
|
|
|
8040
8178
|
return;
|
|
8041
8179
|
}
|
|
8042
8180
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
8043
|
-
const meta = resolveHookMeta(name);
|
|
8181
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
8044
8182
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
8045
8183
|
return;
|
|
8046
8184
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -8061,7 +8199,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
8061
8199
|
}
|
|
8062
8200
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
8063
8201
|
const shortName = shortHookName(name);
|
|
8064
|
-
if (
|
|
8202
|
+
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
8203
|
+
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
8204
|
+
}
|
|
8205
|
+
if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
|
|
8065
8206
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
8066
8207
|
}
|
|
8067
8208
|
if (target === "codewith") {
|
|
@@ -8153,7 +8294,7 @@ function installHook(name, options = {}) {
|
|
|
8153
8294
|
}
|
|
8154
8295
|
if (target === "all") {
|
|
8155
8296
|
const shortName = shortHookName(name);
|
|
8156
|
-
const meta = resolveHookMeta(shortName);
|
|
8297
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
8157
8298
|
if (meta) {
|
|
8158
8299
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
8159
8300
|
if (unsupportedTargets.length > 0) {
|
|
@@ -8186,7 +8327,7 @@ function installHook(name, options = {}) {
|
|
|
8186
8327
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
8187
8328
|
}
|
|
8188
8329
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
8189
|
-
const meta = resolveHookMeta(name);
|
|
8330
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
8190
8331
|
if (!meta)
|
|
8191
8332
|
return;
|
|
8192
8333
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -8203,8 +8344,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
8203
8344
|
const settings = readSettings(scope, target);
|
|
8204
8345
|
if (!settings.hooks)
|
|
8205
8346
|
settings.hooks = {};
|
|
8347
|
+
if (isNativeSafetyName(name)) {
|
|
8348
|
+
for (const groups of Object.values(settings.hooks))
|
|
8349
|
+
for (const group of groups)
|
|
8350
|
+
for (const hook of group.hooks ?? []) {
|
|
8351
|
+
if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
|
|
8352
|
+
throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8206
8356
|
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
8207
|
-
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
8357
|
+
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
8208
8358
|
if (target === "codex") {
|
|
8209
8359
|
const positions = [];
|
|
8210
8360
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -8212,9 +8362,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
8212
8362
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
8213
8363
|
const command = entry.hooks[index]?.command;
|
|
8214
8364
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
8215
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
8365
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
8216
8366
|
if (registrationName(command) !== name)
|
|
8217
8367
|
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
8368
|
+
if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
|
|
8369
|
+
throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
|
|
8218
8370
|
positions.push({ event, entry, index });
|
|
8219
8371
|
}
|
|
8220
8372
|
}
|
|
@@ -8278,12 +8430,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
8278
8430
|
if (target === "codewith") {
|
|
8279
8431
|
const config = readCodewithConfig(scope);
|
|
8280
8432
|
const registered2 = [];
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8433
|
+
try {
|
|
8434
|
+
const parsed = Bun.TOML.parse(config);
|
|
8435
|
+
for (const groups of Object.values(parsed.hooks ?? {})) {
|
|
8436
|
+
if (!Array.isArray(groups))
|
|
8437
|
+
continue;
|
|
8438
|
+
for (const group of groups)
|
|
8439
|
+
for (const hook of group.hooks ?? []) {
|
|
8440
|
+
const name = registrationName(hook.command);
|
|
8441
|
+
if (name)
|
|
8442
|
+
registered2.push(name);
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
} catch {
|
|
8446
|
+
return [];
|
|
8287
8447
|
}
|
|
8288
8448
|
return [...new Set(registered2)];
|
|
8289
8449
|
}
|
|
@@ -8384,10 +8544,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
8384
8544
|
}
|
|
8385
8545
|
function codewithHasHookEntry(name, scope = "global") {
|
|
8386
8546
|
const path = getSettingsPath(scope, "codewith");
|
|
8387
|
-
if (!
|
|
8547
|
+
if (!existsSync9(path))
|
|
8388
8548
|
return false;
|
|
8389
8549
|
try {
|
|
8390
|
-
return codewithHasHookEntryInText(
|
|
8550
|
+
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
8391
8551
|
} catch {
|
|
8392
8552
|
return false;
|
|
8393
8553
|
}
|
|
@@ -8396,12 +8556,12 @@ function removeStoreDirFailClosed(dir) {
|
|
|
8396
8556
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
8397
8557
|
for (const entry of entries) {
|
|
8398
8558
|
if (entry.isDirectory()) {
|
|
8399
|
-
removeStoreDirFailClosed(
|
|
8559
|
+
removeStoreDirFailClosed(join8(dir, entry.name));
|
|
8400
8560
|
}
|
|
8401
8561
|
}
|
|
8402
8562
|
for (const entry of entries) {
|
|
8403
8563
|
if (!entry.isDirectory()) {
|
|
8404
|
-
unlinkSync2(
|
|
8564
|
+
unlinkSync2(join8(dir, entry.name));
|
|
8405
8565
|
}
|
|
8406
8566
|
}
|
|
8407
8567
|
rmdirSync(dir);
|
|
@@ -8432,8 +8592,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8432
8592
|
const registrationsRemaining = [];
|
|
8433
8593
|
if (target === "codewith" || target === "all") {
|
|
8434
8594
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
8435
|
-
if (
|
|
8436
|
-
const before =
|
|
8595
|
+
if (existsSync9(codewithPath)) {
|
|
8596
|
+
const before = readFileSync6(codewithPath, "utf-8");
|
|
8437
8597
|
const after = removeCodewithHookEntry(before, shortName);
|
|
8438
8598
|
if (after.removed) {
|
|
8439
8599
|
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
@@ -8475,7 +8635,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8475
8635
|
// src/index.ts
|
|
8476
8636
|
init_profiles();
|
|
8477
8637
|
init_config();
|
|
8478
|
-
import { existsSync as
|
|
8638
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
8479
8639
|
|
|
8480
8640
|
// src/lib/transport.ts
|
|
8481
8641
|
init_transport();
|
|
@@ -8615,15 +8775,15 @@ init_manifest();
|
|
|
8615
8775
|
// src/lib/custom-install.ts
|
|
8616
8776
|
init_config();
|
|
8617
8777
|
init_manifest();
|
|
8618
|
-
import { existsSync as
|
|
8619
|
-
import { join as
|
|
8778
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync2 } from "fs";
|
|
8779
|
+
import { join as join11 } from "path";
|
|
8620
8780
|
import { tmpdir } from "os";
|
|
8621
8781
|
function manifestAt(dir) {
|
|
8622
|
-
const path =
|
|
8623
|
-
if (!
|
|
8782
|
+
const path = join11(dir, "manifest.json");
|
|
8783
|
+
if (!existsSync11(path))
|
|
8624
8784
|
return;
|
|
8625
8785
|
try {
|
|
8626
|
-
return parseManifest(
|
|
8786
|
+
return parseManifest(readFileSync9(path, "utf-8"));
|
|
8627
8787
|
} catch {
|
|
8628
8788
|
return;
|
|
8629
8789
|
}
|
|
@@ -8642,8 +8802,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
8642
8802
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
8643
8803
|
}
|
|
8644
8804
|
async function cloneToTemp(gitUrl) {
|
|
8645
|
-
const tmp = mkdtempSync(
|
|
8646
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8805
|
+
const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
|
|
8806
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
|
|
8647
8807
|
stdout: "pipe",
|
|
8648
8808
|
stderr: "pipe"
|
|
8649
8809
|
});
|
|
@@ -8656,7 +8816,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
8656
8816
|
rmSync2(tmp, { recursive: true, force: true });
|
|
8657
8817
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
8658
8818
|
}
|
|
8659
|
-
return
|
|
8819
|
+
return join11(tmp, "repo");
|
|
8660
8820
|
}
|
|
8661
8821
|
function isGitUrl(value) {
|
|
8662
8822
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -8666,7 +8826,7 @@ function isCustomSource(value) {
|
|
|
8666
8826
|
return true;
|
|
8667
8827
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
8668
8828
|
return true;
|
|
8669
|
-
return
|
|
8829
|
+
return existsSync11(value);
|
|
8670
8830
|
}
|
|
8671
8831
|
async function installCustomSource(source) {
|
|
8672
8832
|
if (isGitUrl(source)) {
|
|
@@ -8680,11 +8840,11 @@ async function installCustomSource(source) {
|
|
|
8680
8840
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
8681
8841
|
return installManifestUrl(source);
|
|
8682
8842
|
}
|
|
8683
|
-
if (!
|
|
8843
|
+
if (!existsSync11(source)) {
|
|
8684
8844
|
throw new Error(`No such local path: ${source}`);
|
|
8685
8845
|
}
|
|
8686
8846
|
if (source.endsWith("manifest.json")) {
|
|
8687
|
-
const manifest = parseManifest(
|
|
8847
|
+
const manifest = parseManifest(readFileSync9(source, "utf-8"));
|
|
8688
8848
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
8689
8849
|
}
|
|
8690
8850
|
return copyManifestDir(source, source, "local");
|
|
@@ -8714,16 +8874,16 @@ async function installManifestUrl(url) {
|
|
|
8714
8874
|
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
8715
8875
|
}
|
|
8716
8876
|
// src/lib/sync.ts
|
|
8717
|
-
import { createHash as
|
|
8718
|
-
import { readFileSync as
|
|
8877
|
+
import { createHash as createHash3 } from "crypto";
|
|
8878
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8719
8879
|
init_db();
|
|
8720
8880
|
init_store();
|
|
8721
8881
|
init_manifest();
|
|
8722
8882
|
function sha256OfText(text) {
|
|
8723
|
-
return
|
|
8883
|
+
return createHash3("sha256").update(text).digest("hex");
|
|
8724
8884
|
}
|
|
8725
8885
|
function sha256OfPath(path) {
|
|
8726
|
-
return
|
|
8886
|
+
return createHash3("sha256").update(readFileSync10(path)).digest("hex");
|
|
8727
8887
|
}
|
|
8728
8888
|
function collectBundledCatalog() {
|
|
8729
8889
|
const out = [];
|
|
@@ -9024,8 +9184,8 @@ async function installPinnedFromBundled(name, version) {
|
|
|
9024
9184
|
// src/serve.ts
|
|
9025
9185
|
init_manifest();
|
|
9026
9186
|
init_store();
|
|
9027
|
-
import { readFileSync as
|
|
9028
|
-
import { dirname as
|
|
9187
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9188
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
9029
9189
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9030
9190
|
|
|
9031
9191
|
// src/lib/secure-compare.ts
|
|
@@ -9042,155 +9202,356 @@ function secureEqual(a, b) {
|
|
|
9042
9202
|
}
|
|
9043
9203
|
|
|
9044
9204
|
// src/openapi.ts
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9205
|
+
function buildOpenApiDocument(version) {
|
|
9206
|
+
return {
|
|
9207
|
+
openapi: "3.1.0",
|
|
9208
|
+
info: {
|
|
9209
|
+
title: "Hooks Registry API",
|
|
9210
|
+
version,
|
|
9211
|
+
description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
|
|
9212
|
+
},
|
|
9213
|
+
servers: [{ url: "/" }],
|
|
9214
|
+
paths: {
|
|
9215
|
+
"/health": {
|
|
9216
|
+
get: {
|
|
9217
|
+
summary: "Liveness probe",
|
|
9218
|
+
operationId: "getHealth",
|
|
9219
|
+
responses: {
|
|
9220
|
+
"200": {
|
|
9221
|
+
description: "Service is alive",
|
|
9222
|
+
content: {
|
|
9223
|
+
"application/json": {
|
|
9224
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9225
|
+
}
|
|
9064
9226
|
}
|
|
9065
9227
|
}
|
|
9066
9228
|
}
|
|
9067
9229
|
}
|
|
9068
|
-
}
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9230
|
+
},
|
|
9231
|
+
"/ready": {
|
|
9232
|
+
get: {
|
|
9233
|
+
summary: "Readiness probe \u2014 the hooks.lock store is readable",
|
|
9234
|
+
operationId: "getReady",
|
|
9235
|
+
responses: {
|
|
9236
|
+
"200": {
|
|
9237
|
+
description: "Lock store is readable",
|
|
9238
|
+
content: {
|
|
9239
|
+
"application/json": {
|
|
9240
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
},
|
|
9244
|
+
"503": {
|
|
9245
|
+
description: "Lock store is not readable",
|
|
9246
|
+
content: {
|
|
9247
|
+
"application/json": {
|
|
9248
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
}
|
|
9077
9253
|
}
|
|
9078
|
-
}
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9254
|
+
},
|
|
9255
|
+
"/version": {
|
|
9256
|
+
get: {
|
|
9257
|
+
summary: "Package version",
|
|
9258
|
+
operationId: "getVersion",
|
|
9259
|
+
responses: {
|
|
9260
|
+
"200": {
|
|
9261
|
+
description: "The installed @hasna/hooks version",
|
|
9262
|
+
content: {
|
|
9263
|
+
"application/json": {
|
|
9264
|
+
schema: { $ref: "#/components/schemas/HooksVersion" }
|
|
9265
|
+
}
|
|
9090
9266
|
}
|
|
9091
9267
|
}
|
|
9092
9268
|
}
|
|
9093
9269
|
}
|
|
9094
|
-
}
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
summary: "List the full hook catalog with versions and sha256",
|
|
9106
|
-
operationId: "getCatalog",
|
|
9107
|
-
responses: {
|
|
9108
|
-
"200": {
|
|
9109
|
-
description: "Catalog entries",
|
|
9110
|
-
content: {
|
|
9111
|
-
"application/json": {
|
|
9112
|
-
schema: {
|
|
9113
|
-
type: "object",
|
|
9114
|
-
properties: {
|
|
9115
|
-
hooks: {
|
|
9116
|
-
type: "array",
|
|
9117
|
-
items: {
|
|
9118
|
-
type: "object",
|
|
9119
|
-
properties: {
|
|
9120
|
-
name: { type: "string" },
|
|
9121
|
-
version: { type: "string" },
|
|
9122
|
-
sha256: { type: "string" },
|
|
9123
|
-
events: { type: "array", items: { type: "string" } }
|
|
9124
|
-
}
|
|
9125
|
-
}
|
|
9126
|
-
}
|
|
9127
|
-
}
|
|
9270
|
+
},
|
|
9271
|
+
"/openapi.json": {
|
|
9272
|
+
get: {
|
|
9273
|
+
summary: "This OpenAPI document",
|
|
9274
|
+
operationId: "getOpenApi",
|
|
9275
|
+
responses: {
|
|
9276
|
+
"200": {
|
|
9277
|
+
description: "OpenAPI 3.1 document",
|
|
9278
|
+
content: {
|
|
9279
|
+
"application/json": {
|
|
9280
|
+
schema: { type: "object", additionalProperties: true }
|
|
9128
9281
|
}
|
|
9129
9282
|
}
|
|
9130
9283
|
}
|
|
9131
9284
|
}
|
|
9132
9285
|
}
|
|
9133
|
-
}
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9286
|
+
},
|
|
9287
|
+
"/api/v1/catalog": {
|
|
9288
|
+
get: {
|
|
9289
|
+
summary: "List the full hook catalog with versions and sha256",
|
|
9290
|
+
operationId: "getCatalog",
|
|
9291
|
+
responses: {
|
|
9292
|
+
"200": {
|
|
9293
|
+
description: "Catalog entries",
|
|
9294
|
+
content: {
|
|
9295
|
+
"application/json": {
|
|
9296
|
+
schema: { $ref: "#/components/schemas/HooksCatalog" }
|
|
9297
|
+
}
|
|
9298
|
+
}
|
|
9299
|
+
}
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
},
|
|
9303
|
+
"/api/v1/lock": {
|
|
9304
|
+
get: {
|
|
9305
|
+
summary: "Read the published hooks.lock with per-hook versions",
|
|
9306
|
+
operationId: "getLock",
|
|
9307
|
+
responses: {
|
|
9308
|
+
"200": {
|
|
9309
|
+
description: "The lock file",
|
|
9310
|
+
content: {
|
|
9311
|
+
"application/json": {
|
|
9312
|
+
schema: { $ref: "#/components/schemas/HooksLock" }
|
|
9313
|
+
}
|
|
9314
|
+
}
|
|
9315
|
+
}
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
},
|
|
9319
|
+
"/api/v1/hooks/{name}/{version}": {
|
|
9320
|
+
get: {
|
|
9321
|
+
summary: "Fetch one hook's manifest and script",
|
|
9322
|
+
operationId: "getHookArtifact",
|
|
9323
|
+
parameters: [
|
|
9324
|
+
{ name: "name", in: "path", required: true, schema: { type: "string" } },
|
|
9325
|
+
{ name: "version", in: "path", required: true, schema: { type: "string" } }
|
|
9326
|
+
],
|
|
9327
|
+
responses: {
|
|
9328
|
+
"200": {
|
|
9329
|
+
description: "Hook artifact; x-hook-sha256 header carries the script hash",
|
|
9330
|
+
headers: {
|
|
9331
|
+
"x-hook-sha256": {
|
|
9332
|
+
description: "sha256 of the served script, the same value the lock pins",
|
|
9333
|
+
schema: { type: "string" }
|
|
9334
|
+
}
|
|
9335
|
+
},
|
|
9336
|
+
content: {
|
|
9337
|
+
"application/json": {
|
|
9338
|
+
schema: { $ref: "#/components/schemas/HooksArtifact" }
|
|
9339
|
+
}
|
|
9340
|
+
}
|
|
9341
|
+
},
|
|
9342
|
+
"400": {
|
|
9343
|
+
description: "Invalid URL encoding or semver version",
|
|
9344
|
+
content: {
|
|
9345
|
+
"application/json": {
|
|
9346
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
},
|
|
9350
|
+
"404": {
|
|
9351
|
+
description: "Hook not found locally",
|
|
9352
|
+
content: {
|
|
9353
|
+
"application/json": {
|
|
9354
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9355
|
+
}
|
|
9356
|
+
}
|
|
9357
|
+
}
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
},
|
|
9361
|
+
"/api/v1/hooks": {
|
|
9362
|
+
put: {
|
|
9363
|
+
summary: "Publish/retrust a hook (requires the API key)",
|
|
9364
|
+
operationId: "putHook",
|
|
9365
|
+
requestBody: {
|
|
9366
|
+
required: true,
|
|
9367
|
+
content: {
|
|
9368
|
+
"application/json": {
|
|
9369
|
+
schema: { $ref: "#/components/schemas/HooksPublishRequest" }
|
|
9370
|
+
}
|
|
9371
|
+
}
|
|
9372
|
+
},
|
|
9373
|
+
responses: {
|
|
9374
|
+
"200": {
|
|
9375
|
+
description: "Hook retrusted and pin updated",
|
|
9376
|
+
content: {
|
|
9377
|
+
"application/json": {
|
|
9378
|
+
schema: { $ref: "#/components/schemas/HooksPublishResponse" }
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
},
|
|
9382
|
+
"400": {
|
|
9383
|
+
description: "Invalid JSON body",
|
|
9384
|
+
content: {
|
|
9385
|
+
"application/json": {
|
|
9386
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
},
|
|
9390
|
+
"401": {
|
|
9391
|
+
description: "Missing or invalid API key",
|
|
9392
|
+
content: {
|
|
9393
|
+
"application/json": {
|
|
9394
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
},
|
|
9398
|
+
"404": {
|
|
9399
|
+
description: "Hook not found in local store",
|
|
9400
|
+
content: {
|
|
9401
|
+
"application/json": {
|
|
9402
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9403
|
+
}
|
|
9404
|
+
}
|
|
9405
|
+
},
|
|
9406
|
+
"409": {
|
|
9407
|
+
description: "Version mismatch with the local store",
|
|
9408
|
+
content: {
|
|
9409
|
+
"application/json": {
|
|
9410
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9411
|
+
}
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9153
9414
|
},
|
|
9154
|
-
|
|
9155
|
-
"404": { description: "Hook not found locally" }
|
|
9415
|
+
security: [{ apiKey: [] }]
|
|
9156
9416
|
}
|
|
9157
9417
|
}
|
|
9158
9418
|
},
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9419
|
+
components: {
|
|
9420
|
+
securitySchemes: {
|
|
9421
|
+
apiKey: {
|
|
9422
|
+
type: "http",
|
|
9423
|
+
scheme: "bearer",
|
|
9424
|
+
description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
|
|
9425
|
+
}
|
|
9426
|
+
},
|
|
9427
|
+
schemas: {
|
|
9428
|
+
HooksHealth: {
|
|
9429
|
+
type: "object",
|
|
9430
|
+
additionalProperties: false,
|
|
9431
|
+
required: ["status", "name"],
|
|
9432
|
+
properties: {
|
|
9433
|
+
status: { type: "string" },
|
|
9434
|
+
name: { type: "string" }
|
|
9435
|
+
}
|
|
9436
|
+
},
|
|
9437
|
+
HooksVersion: {
|
|
9438
|
+
type: "object",
|
|
9439
|
+
additionalProperties: false,
|
|
9440
|
+
required: ["version"],
|
|
9441
|
+
properties: { version: { type: "string" } }
|
|
9442
|
+
},
|
|
9443
|
+
HooksCatalogEntry: {
|
|
9444
|
+
type: "object",
|
|
9445
|
+
additionalProperties: false,
|
|
9446
|
+
required: ["name", "version", "sha256", "events", "description", "source", "versions"],
|
|
9447
|
+
properties: {
|
|
9448
|
+
name: { type: "string" },
|
|
9449
|
+
version: { type: "string" },
|
|
9450
|
+
sha256: { type: "string" },
|
|
9451
|
+
events: { type: "array", items: { type: "string" } },
|
|
9452
|
+
description: { type: "string" },
|
|
9453
|
+
source: {
|
|
9454
|
+
type: "string",
|
|
9455
|
+
description: "bundled | custom | custom-overrides-bundled"
|
|
9456
|
+
},
|
|
9457
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9458
|
+
}
|
|
9459
|
+
},
|
|
9460
|
+
HooksCatalog: {
|
|
9461
|
+
type: "object",
|
|
9462
|
+
additionalProperties: false,
|
|
9463
|
+
required: ["hooks"],
|
|
9464
|
+
properties: {
|
|
9465
|
+
hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
|
|
9466
|
+
}
|
|
9467
|
+
},
|
|
9468
|
+
HooksLockEntry: {
|
|
9469
|
+
type: "object",
|
|
9470
|
+
additionalProperties: false,
|
|
9471
|
+
required: ["version", "sha256", "source"],
|
|
9472
|
+
properties: {
|
|
9473
|
+
version: { type: "string" },
|
|
9474
|
+
sha256: { type: "string" },
|
|
9475
|
+
source: { type: "string" },
|
|
9476
|
+
pinned: {
|
|
9477
|
+
type: "boolean",
|
|
9478
|
+
description: "True for an explicit install/update pin, absent for sync-maintained pins."
|
|
9479
|
+
},
|
|
9480
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9481
|
+
}
|
|
9482
|
+
},
|
|
9483
|
+
HooksLock: {
|
|
9484
|
+
type: "object",
|
|
9485
|
+
additionalProperties: false,
|
|
9486
|
+
required: ["hooks"],
|
|
9487
|
+
properties: {
|
|
9488
|
+
hooks: {
|
|
9489
|
+
type: "object",
|
|
9490
|
+
additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
|
|
9171
9491
|
}
|
|
9172
9492
|
}
|
|
9173
9493
|
},
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
"
|
|
9178
|
-
|
|
9494
|
+
HooksManifest: {
|
|
9495
|
+
type: "object",
|
|
9496
|
+
additionalProperties: false,
|
|
9497
|
+
required: ["name", "version", "events", "script"],
|
|
9498
|
+
properties: {
|
|
9499
|
+
name: { type: "string" },
|
|
9500
|
+
version: { type: "string" },
|
|
9501
|
+
description: { type: "string" },
|
|
9502
|
+
events: { type: "array", items: { type: "string" } },
|
|
9503
|
+
script: { type: "string" },
|
|
9504
|
+
script_kind: { type: "string", enum: ["inline", "file"] },
|
|
9505
|
+
args: { type: "array", items: { type: "string" } },
|
|
9506
|
+
timeout_ms: { type: "number" }
|
|
9507
|
+
}
|
|
9179
9508
|
},
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9509
|
+
HooksArtifact: {
|
|
9510
|
+
type: "object",
|
|
9511
|
+
additionalProperties: false,
|
|
9512
|
+
required: ["manifest", "script"],
|
|
9513
|
+
properties: {
|
|
9514
|
+
manifest: { $ref: "#/components/schemas/HooksManifest" },
|
|
9515
|
+
script: { type: "string" }
|
|
9516
|
+
}
|
|
9517
|
+
},
|
|
9518
|
+
HooksError: {
|
|
9519
|
+
type: "object",
|
|
9520
|
+
additionalProperties: false,
|
|
9521
|
+
required: ["error"],
|
|
9522
|
+
properties: { error: { type: "string" } }
|
|
9523
|
+
},
|
|
9524
|
+
HooksPublishRequest: {
|
|
9525
|
+
type: "object",
|
|
9526
|
+
additionalProperties: false,
|
|
9527
|
+
required: ["name"],
|
|
9528
|
+
properties: {
|
|
9529
|
+
name: { type: "string" },
|
|
9530
|
+
version: { type: "string" }
|
|
9531
|
+
}
|
|
9532
|
+
},
|
|
9533
|
+
HooksPublishResponse: {
|
|
9534
|
+
type: "object",
|
|
9535
|
+
additionalProperties: false,
|
|
9536
|
+
required: ["ok", "hook"],
|
|
9537
|
+
properties: {
|
|
9538
|
+
ok: { type: "boolean" },
|
|
9539
|
+
hook: {
|
|
9540
|
+
type: "object",
|
|
9541
|
+
additionalProperties: false,
|
|
9542
|
+
required: ["name", "version", "sha256"],
|
|
9543
|
+
properties: {
|
|
9544
|
+
name: { type: "string" },
|
|
9545
|
+
version: { type: "string" },
|
|
9546
|
+
sha256: { type: "string" }
|
|
9547
|
+
}
|
|
9548
|
+
}
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9190
9551
|
}
|
|
9191
9552
|
}
|
|
9192
|
-
}
|
|
9193
|
-
}
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
9194
9555
|
|
|
9195
9556
|
// src/serve.ts
|
|
9196
9557
|
init_semver();
|
|
@@ -9199,8 +9560,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
9199
9560
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
9200
9561
|
function packageVersion() {
|
|
9201
9562
|
try {
|
|
9202
|
-
const pkgPath =
|
|
9203
|
-
return JSON.parse(
|
|
9563
|
+
const pkgPath = join12(dirname7(fileURLToPath4(import.meta.url)), "..", "package.json");
|
|
9564
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
|
|
9204
9565
|
} catch {
|
|
9205
9566
|
return "0.0.0";
|
|
9206
9567
|
}
|
|
@@ -9289,7 +9650,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
9289
9650
|
return Promise.resolve(json({ version: packageVersion() }));
|
|
9290
9651
|
}
|
|
9291
9652
|
if (url.pathname === "/openapi.json" && req.method === "GET") {
|
|
9292
|
-
return Promise.resolve(json(
|
|
9653
|
+
return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
|
|
9293
9654
|
}
|
|
9294
9655
|
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
9295
9656
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
@@ -9466,11 +9827,11 @@ async function runHook(name, input, options = {}) {
|
|
|
9466
9827
|
throw new Error(`Hook '${name}' not found`);
|
|
9467
9828
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
9468
9829
|
const resolvedScript = custom2?.scriptPath;
|
|
9469
|
-
const script = resolvedScript ?? (
|
|
9830
|
+
const script = resolvedScript ?? (existsSync13(hookScript) ? hookScript : undefined);
|
|
9470
9831
|
if (!script)
|
|
9471
9832
|
throw new Error(`Hook script not found: ${name}`);
|
|
9472
9833
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
9473
|
-
const content =
|
|
9834
|
+
const content = readFileSync12(script);
|
|
9474
9835
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
9475
9836
|
if (!check.ok) {
|
|
9476
9837
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|