@hasna/hooks 0.9.4 → 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 +40 -2
- package/bin/hooks-mcp.js +330 -149
- package/bin/index.js +974 -350
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +559 -165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +902 -290
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/lib/resolver-types.d.ts +1 -1
- package/dist/lib/sync.d.ts +3 -2
- package/dist/lib/transport.d.ts +5 -1
- 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 +16 -17
- package/dist/sdk/index.js +447 -47
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +8 -5
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +104 -2
- 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
|
}
|
|
@@ -5101,14 +5101,19 @@ var init_profiles = __esm(() => {
|
|
|
5101
5101
|
init_app_home();
|
|
5102
5102
|
});
|
|
5103
5103
|
|
|
5104
|
-
//
|
|
5104
|
+
// ../contracts/dist/client/transport.js
|
|
5105
5105
|
import { isIP } from "net";
|
|
5106
5106
|
import { spawnSync } from "child_process";
|
|
5107
|
-
import { closeSync as
|
|
5108
|
-
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5109
|
-
import { createRequire } from "module";
|
|
5107
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
5108
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
5110
5109
|
import { hostname as osHostname } from "os";
|
|
5111
|
-
import { isAbsolute as
|
|
5110
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
5111
|
+
import { createRequire } from "module";
|
|
5112
|
+
import { fileURLToPath as fileURLToPath22 } from "url";
|
|
5113
|
+
import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
|
|
5114
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5115
|
+
import { basename as basename2, dirname as dirname6, join as join10, relative, sep as sep2 } from "path";
|
|
5116
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
5112
5117
|
function envToken(name) {
|
|
5113
5118
|
return name.toUpperCase().replace(/-/g, "_");
|
|
5114
5119
|
}
|
|
@@ -5125,27 +5130,133 @@ function credentialOverrideEnvKey(name) {
|
|
|
5125
5130
|
function credentialPointerEnvKey(name) {
|
|
5126
5131
|
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
5127
5132
|
}
|
|
5133
|
+
function e(e2, n, r) {
|
|
5134
|
+
throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
|
|
5135
|
+
}
|
|
5136
|
+
function n(n2, i, o, f) {
|
|
5137
|
+
let s, u, l = r(n2, o), c = function(e2) {
|
|
5138
|
+
let n3 = new Set(["default", ...e2.conditions || []]);
|
|
5139
|
+
return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
|
|
5140
|
+
}(f || {}), a = i[l];
|
|
5141
|
+
if (a === undefined) {
|
|
5142
|
+
let e2, n3, r, t;
|
|
5143
|
+
for (t in i)
|
|
5144
|
+
n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
|
|
5145
|
+
a = i[n3];
|
|
5146
|
+
}
|
|
5147
|
+
return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
|
|
5148
|
+
let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
|
|
5149
|
+
for (;t < i2; t++)
|
|
5150
|
+
e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
|
|
5151
|
+
}(s, u), s;
|
|
5152
|
+
}
|
|
5153
|
+
function r(e2, n2, r2) {
|
|
5154
|
+
if (e2 === n2 || n2 === ".")
|
|
5155
|
+
return ".";
|
|
5156
|
+
let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
|
|
5157
|
+
return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
|
|
5158
|
+
}
|
|
5159
|
+
function t(e2, n2, r2) {
|
|
5160
|
+
if (e2) {
|
|
5161
|
+
if (typeof e2 == "string")
|
|
5162
|
+
return r2 && r2.add(e2), [e2];
|
|
5163
|
+
let i, o;
|
|
5164
|
+
if (Array.isArray(e2)) {
|
|
5165
|
+
for (o = r2 || new Set, i = 0;i < e2.length; i++)
|
|
5166
|
+
t(e2[i], n2, o);
|
|
5167
|
+
if (!r2 && o.size)
|
|
5168
|
+
return [...o];
|
|
5169
|
+
} else
|
|
5170
|
+
for (i in e2)
|
|
5171
|
+
if (n2.has(i))
|
|
5172
|
+
return t(e2[i], n2, r2);
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
function o(e2, r2, t2) {
|
|
5176
|
+
let i, o2 = e2.exports;
|
|
5177
|
+
if (o2) {
|
|
5178
|
+
if (typeof o2 == "string")
|
|
5179
|
+
o2 = { ".": o2 };
|
|
5180
|
+
else
|
|
5181
|
+
for (i in o2) {
|
|
5182
|
+
i[0] !== "." && (o2 = { ".": o2 });
|
|
5183
|
+
break;
|
|
5184
|
+
}
|
|
5185
|
+
return n(e2.name, o2, r2 || ".", t2);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
function entryPoint(directory) {
|
|
5189
|
+
const root = realpathSync3(directory);
|
|
5190
|
+
const fd = openSync3(join10(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
5191
|
+
let pkg;
|
|
5192
|
+
try {
|
|
5193
|
+
const stat = fstatSync3(fd);
|
|
5194
|
+
if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
|
|
5195
|
+
throw new Error("Invalid Secrets package metadata");
|
|
5196
|
+
pkg = JSON.parse(readFileSync8(fd, "utf8"));
|
|
5197
|
+
} finally {
|
|
5198
|
+
closeSync3(fd);
|
|
5199
|
+
}
|
|
5200
|
+
if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
|
|
5201
|
+
throw new Error("Invalid Secrets package identity");
|
|
5202
|
+
}
|
|
5203
|
+
const target = o(pkg, ".")?.[0];
|
|
5204
|
+
if (typeof target !== "string" || !target.startsWith("./"))
|
|
5205
|
+
throw new Error("Missing Secrets import export");
|
|
5206
|
+
const parts = target.slice(2).split("/");
|
|
5207
|
+
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
5208
|
+
throw new Error("Invalid Secrets import export");
|
|
5209
|
+
}
|
|
5210
|
+
const file = realpathSync3(join10(root, ...parts));
|
|
5211
|
+
const within = relative(root, file);
|
|
5212
|
+
if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
|
|
5213
|
+
throw new Error("Secrets import export escapes its package or is not a file");
|
|
5214
|
+
}
|
|
5215
|
+
return pathToFileURL(file).href;
|
|
5216
|
+
}
|
|
5217
|
+
function resolveInstalledSecrets(parentUrl) {
|
|
5218
|
+
let directory = dirname6(fileURLToPath3(parentUrl));
|
|
5219
|
+
for (;; ) {
|
|
5220
|
+
if (basename2(directory) !== "node_modules") {
|
|
5221
|
+
const candidate = join10(directory, "node_modules", PACKAGE);
|
|
5222
|
+
let absent = false;
|
|
5223
|
+
try {
|
|
5224
|
+
lstatSync3(candidate);
|
|
5225
|
+
} catch (error) {
|
|
5226
|
+
if (error.code !== "ENOENT")
|
|
5227
|
+
throw error;
|
|
5228
|
+
absent = true;
|
|
5229
|
+
}
|
|
5230
|
+
if (!absent)
|
|
5231
|
+
return entryPoint(candidate);
|
|
5232
|
+
}
|
|
5233
|
+
const parent = dirname6(directory);
|
|
5234
|
+
if (parent === directory)
|
|
5235
|
+
throw new Error("Secrets SDK is not installed for this consumer");
|
|
5236
|
+
directory = parent;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5128
5239
|
function homeDir(env) {
|
|
5129
5240
|
const home = env.HOME?.trim();
|
|
5130
5241
|
return home ? home : null;
|
|
5131
5242
|
}
|
|
5132
5243
|
function absoluteOverride(env, key) {
|
|
5133
5244
|
const value = env[key]?.trim();
|
|
5134
|
-
return value &&
|
|
5245
|
+
return value && isAbsolute3(value) ? value : null;
|
|
5135
5246
|
}
|
|
5136
5247
|
function hasnaHomeDir(env) {
|
|
5137
5248
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
5138
5249
|
if (override)
|
|
5139
5250
|
return override;
|
|
5140
5251
|
const home = homeDir(env);
|
|
5141
|
-
return home ?
|
|
5252
|
+
return home ? join22(home, HASNA_HOME_DIR) : null;
|
|
5142
5253
|
}
|
|
5143
5254
|
function appConfigDir(name, env) {
|
|
5144
5255
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
5145
5256
|
if (configRoot)
|
|
5146
|
-
return
|
|
5257
|
+
return join22(configRoot, name);
|
|
5147
5258
|
const root = hasnaHomeDir(env);
|
|
5148
|
-
return root ?
|
|
5259
|
+
return root ? join22(root, name, CONFIG_SUBDIR) : null;
|
|
5149
5260
|
}
|
|
5150
5261
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
5151
5262
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -5154,7 +5265,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
5154
5265
|
if (!directory)
|
|
5155
5266
|
return [];
|
|
5156
5267
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
5157
|
-
return [{ path:
|
|
5268
|
+
return [{ path: join22(directory, file), tier: "disk" }];
|
|
5158
5269
|
}
|
|
5159
5270
|
function credentialDiskSources(name, env) {
|
|
5160
5271
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -5177,9 +5288,9 @@ function parseEnvFile(text) {
|
|
|
5177
5288
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5178
5289
|
continue;
|
|
5179
5290
|
let value = withoutExport.slice(equals + 1).trim();
|
|
5180
|
-
const
|
|
5181
|
-
if (
|
|
5182
|
-
if (value.length < 2 || !value.endsWith(
|
|
5291
|
+
const quote2 = value[0];
|
|
5292
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
5293
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
5183
5294
|
unusable.add(key);
|
|
5184
5295
|
continue;
|
|
5185
5296
|
}
|
|
@@ -5208,7 +5319,7 @@ function readAppConfigFile(path) {
|
|
|
5208
5319
|
};
|
|
5209
5320
|
let fd = -1;
|
|
5210
5321
|
try {
|
|
5211
|
-
fd =
|
|
5322
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
5212
5323
|
} catch (error) {
|
|
5213
5324
|
const code = error.code;
|
|
5214
5325
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -5218,7 +5329,7 @@ function readAppConfigFile(path) {
|
|
|
5218
5329
|
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
5219
5330
|
}
|
|
5220
5331
|
try {
|
|
5221
|
-
const before =
|
|
5332
|
+
const before = fstatSync2(fd);
|
|
5222
5333
|
if (!before.isFile())
|
|
5223
5334
|
unsafe("the path is not a regular file");
|
|
5224
5335
|
if (!configFileModeAllowed(before.mode)) {
|
|
@@ -5229,31 +5340,39 @@ function readAppConfigFile(path) {
|
|
|
5229
5340
|
unsafe("the file is not owned by the current user");
|
|
5230
5341
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
5231
5342
|
unsafe("the file exceeds the size limit");
|
|
5232
|
-
const bytes =
|
|
5233
|
-
const after =
|
|
5343
|
+
const bytes = readFileSync22(fd);
|
|
5344
|
+
const after = fstatSync2(fd);
|
|
5234
5345
|
if (!configFileReadsCoherent(before, after)) {
|
|
5235
5346
|
unsafe("the file changed while being read");
|
|
5236
5347
|
}
|
|
5237
5348
|
return parseEnvFile(bytes.toString("utf8"));
|
|
5238
5349
|
} finally {
|
|
5239
5350
|
if (fd !== -1)
|
|
5240
|
-
|
|
5351
|
+
closeSync22(fd);
|
|
5241
5352
|
}
|
|
5242
5353
|
}
|
|
5243
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
5354
|
+
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
5244
5355
|
const parsed = readAppConfigFile(path);
|
|
5245
5356
|
if (!parsed)
|
|
5246
5357
|
return null;
|
|
5247
|
-
for (const key of apiKeyKeys) {
|
|
5358
|
+
for (const key of [...apiKeyKeys, pointerKey]) {
|
|
5248
5359
|
if (parsed.unusable.has(key)) {
|
|
5249
5360
|
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
5250
5361
|
}
|
|
5251
5362
|
}
|
|
5252
5363
|
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
5364
|
+
const pointer = parsed.values.get(pointerKey)?.trim();
|
|
5365
|
+
if (pointer !== undefined) {
|
|
5366
|
+
if (!VAULT_POINTER_SHAPE.test(pointer))
|
|
5367
|
+
throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
|
|
5368
|
+
if (values.length)
|
|
5369
|
+
throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
|
|
5370
|
+
return { apiKey: "", pointerVaultKey: pointer };
|
|
5371
|
+
}
|
|
5253
5372
|
if (new Set(values).size > 1) {
|
|
5254
5373
|
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
5255
5374
|
}
|
|
5256
|
-
return values[0]
|
|
5375
|
+
return values[0] === undefined ? null : { apiKey: values[0] };
|
|
5257
5376
|
}
|
|
5258
5377
|
function appConfigDiskValue(name, env, keys) {
|
|
5259
5378
|
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
@@ -5325,6 +5444,18 @@ function sealCredential(fields) {
|
|
|
5325
5444
|
});
|
|
5326
5445
|
return Object.freeze(sealed);
|
|
5327
5446
|
}
|
|
5447
|
+
function explicitCredential(appName, apiKey) {
|
|
5448
|
+
const source = "explicit apiKey option";
|
|
5449
|
+
assertUsableCredential(appName, source, apiKey);
|
|
5450
|
+
return sealCredential({
|
|
5451
|
+
apiKey,
|
|
5452
|
+
tier: "argument",
|
|
5453
|
+
source,
|
|
5454
|
+
deliberate: true,
|
|
5455
|
+
diskCandidates: [],
|
|
5456
|
+
warning: null
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
5328
5459
|
function firstEnvValue(env, keys) {
|
|
5329
5460
|
for (const key of keys) {
|
|
5330
5461
|
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
@@ -5510,12 +5641,13 @@ function resolveCredential(name, env, options = {}) {
|
|
|
5510
5641
|
}
|
|
5511
5642
|
const paths = profileDiskSources(name, env, profile);
|
|
5512
5643
|
for (const path of paths) {
|
|
5513
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
5644
|
+
const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
|
|
5514
5645
|
if (value) {
|
|
5515
|
-
|
|
5646
|
+
if (!value.pointerVaultKey)
|
|
5647
|
+
assertUsableCredential(name, path, value.apiKey);
|
|
5516
5648
|
return sealCredential({
|
|
5517
|
-
|
|
5518
|
-
tier: "profile",
|
|
5649
|
+
...value,
|
|
5650
|
+
tier: value.pointerVaultKey ? "pointer" : "profile",
|
|
5519
5651
|
source: path,
|
|
5520
5652
|
deliberate: true,
|
|
5521
5653
|
diskCandidates: paths,
|
|
@@ -5523,7 +5655,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
5523
5655
|
});
|
|
5524
5656
|
}
|
|
5525
5657
|
}
|
|
5526
|
-
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
5658
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
5527
5659
|
}
|
|
5528
5660
|
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
5529
5661
|
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
@@ -5548,17 +5680,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
5548
5680
|
});
|
|
5549
5681
|
}
|
|
5550
5682
|
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
5551
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
5683
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
|
|
5552
5684
|
if (diskHits.length > 0) {
|
|
5553
5685
|
const winner = diskHits[0];
|
|
5554
|
-
|
|
5686
|
+
if (winner.value.pointerVaultKey) {
|
|
5687
|
+
return sealCredential({
|
|
5688
|
+
...winner.value,
|
|
5689
|
+
tier: "pointer",
|
|
5690
|
+
source: winner.src.path,
|
|
5691
|
+
deliberate: false,
|
|
5692
|
+
diskCandidates: diskPaths,
|
|
5693
|
+
warning: null
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5696
|
+
assertUsableCredential(name, winner.src.path, winner.value.apiKey);
|
|
5555
5697
|
const divergentSources = [
|
|
5556
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
5557
|
-
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
5698
|
+
...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
|
|
5699
|
+
...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
|
|
5558
5700
|
];
|
|
5559
5701
|
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
5560
5702
|
return sealCredential({
|
|
5561
|
-
apiKey: winner.value,
|
|
5703
|
+
apiKey: winner.value.apiKey,
|
|
5562
5704
|
tier: winner.src.tier,
|
|
5563
5705
|
source: winner.src.path,
|
|
5564
5706
|
deliberate: false,
|
|
@@ -5579,6 +5721,53 @@ function resolveCredential(name, env, options = {}) {
|
|
|
5579
5721
|
}
|
|
5580
5722
|
return null;
|
|
5581
5723
|
}
|
|
5724
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
5725
|
+
const secretsEnv = snapshotClientEnvironment("secrets", env);
|
|
5726
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
5727
|
+
const pointerEnvKey = pointerResolution.source;
|
|
5728
|
+
if (!vaultKey) {
|
|
5729
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
5730
|
+
}
|
|
5731
|
+
if (name === "secrets") {
|
|
5732
|
+
throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
|
|
5733
|
+
}
|
|
5734
|
+
let secretsSdk;
|
|
5735
|
+
try {
|
|
5736
|
+
const sdkUrl = resolveInstalledSecrets(import.meta.url);
|
|
5737
|
+
secretsSdk = requireSecretsSdk(fileURLToPath22(sdkUrl));
|
|
5738
|
+
} catch {
|
|
5739
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
5740
|
+
}
|
|
5741
|
+
let client;
|
|
5742
|
+
try {
|
|
5743
|
+
const bootstrap = resolveCredential("secrets", secretsEnv);
|
|
5744
|
+
if (!bootstrap || bootstrap.tier === "pointer") {
|
|
5745
|
+
throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
|
|
5746
|
+
}
|
|
5747
|
+
client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
|
|
5748
|
+
} catch {
|
|
5749
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
5750
|
+
}
|
|
5751
|
+
let secret;
|
|
5752
|
+
try {
|
|
5753
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
5754
|
+
} catch {
|
|
5755
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
5756
|
+
}
|
|
5757
|
+
const value = secret.value;
|
|
5758
|
+
if (!value) {
|
|
5759
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
5760
|
+
}
|
|
5761
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
5762
|
+
return sealCredential({
|
|
5763
|
+
apiKey: value,
|
|
5764
|
+
tier: "pointer",
|
|
5765
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
5766
|
+
deliberate: pointerResolution.deliberate,
|
|
5767
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
5768
|
+
warning: null
|
|
5769
|
+
});
|
|
5770
|
+
}
|
|
5582
5771
|
function defaultFleetGatewayBaseUrl(name) {
|
|
5583
5772
|
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
5584
5773
|
}
|
|
@@ -5781,8 +5970,9 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
5781
5970
|
const paths = credentialDiskSources(name, env);
|
|
5782
5971
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
5783
5972
|
}
|
|
5784
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT,
|
|
5973
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", MAX_PACKAGE_BYTES, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
5785
5974
|
var init_transport = __esm(() => {
|
|
5975
|
+
MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
5786
5976
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
5787
5977
|
appName;
|
|
5788
5978
|
attempted;
|
|
@@ -5810,7 +6000,6 @@ var init_transport = __esm(() => {
|
|
|
5810
6000
|
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
5811
6001
|
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
5812
6002
|
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
5813
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
5814
6003
|
requireSecretsSdk = createRequire(import.meta.url);
|
|
5815
6004
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
5816
6005
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -6013,12 +6202,12 @@ var init_db_writer = __esm(() => {
|
|
|
6013
6202
|
});
|
|
6014
6203
|
|
|
6015
6204
|
// src/lib/hook-env.ts
|
|
6016
|
-
import { dirname as
|
|
6017
|
-
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";
|
|
6018
6207
|
import { tmpdir as tmpdir2 } from "os";
|
|
6019
6208
|
function isDeniedEnvName(name) {
|
|
6020
6209
|
const upper = name.toUpperCase();
|
|
6021
|
-
if (DENY_PREFIXES.some((
|
|
6210
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
6022
6211
|
return true;
|
|
6023
6212
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
6024
6213
|
return true;
|
|
@@ -6027,13 +6216,13 @@ function isDeniedEnvName(name) {
|
|
|
6027
6216
|
function isInterpreterInjectionEnvName(name) {
|
|
6028
6217
|
if (INTERPRETER_INJECTION.has(name))
|
|
6029
6218
|
return true;
|
|
6030
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
6219
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
6031
6220
|
}
|
|
6032
6221
|
function isBashFunctionEnvName(name) {
|
|
6033
6222
|
return name.startsWith("BASH_FUNC_");
|
|
6034
6223
|
}
|
|
6035
6224
|
function isUnsafePathEntry(entry, home) {
|
|
6036
|
-
if (entry === "" || !
|
|
6225
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
6037
6226
|
return true;
|
|
6038
6227
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
6039
6228
|
return true;
|
|
@@ -6043,7 +6232,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6043
6232
|
return true;
|
|
6044
6233
|
const canonical = (path) => {
|
|
6045
6234
|
try {
|
|
6046
|
-
return
|
|
6235
|
+
return realpathSync4(path);
|
|
6047
6236
|
} catch {
|
|
6048
6237
|
return path;
|
|
6049
6238
|
}
|
|
@@ -6055,16 +6244,16 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6055
6244
|
return true;
|
|
6056
6245
|
}
|
|
6057
6246
|
try {
|
|
6058
|
-
if ((
|
|
6247
|
+
if ((statSync2(entry).mode & 2) !== 0)
|
|
6059
6248
|
return true;
|
|
6060
6249
|
} catch {}
|
|
6061
6250
|
return false;
|
|
6062
6251
|
}
|
|
6063
6252
|
function sanitizeHookPath(pathValue, home) {
|
|
6064
6253
|
const baseline = PATH_BASELINE.slice();
|
|
6065
|
-
if (process.platform === "darwin" &&
|
|
6254
|
+
if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
|
|
6066
6255
|
baseline.push("/opt/homebrew/bin");
|
|
6067
|
-
const bunDir =
|
|
6256
|
+
const bunDir = dirname8(process.execPath);
|
|
6068
6257
|
const seen = new Set;
|
|
6069
6258
|
const out = [];
|
|
6070
6259
|
for (const dir of baseline) {
|
|
@@ -6189,9 +6378,9 @@ __export(exports_run, {
|
|
|
6189
6378
|
});
|
|
6190
6379
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6191
6380
|
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6192
|
-
import { basename as
|
|
6381
|
+
import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute5, join as join13 } from "path";
|
|
6193
6382
|
function extensionOf(scriptPath) {
|
|
6194
|
-
const base =
|
|
6383
|
+
const base = basename3(scriptPath);
|
|
6195
6384
|
const dot = base.lastIndexOf(".");
|
|
6196
6385
|
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
6197
6386
|
}
|
|
@@ -6203,9 +6392,9 @@ function shebangInterpreter(firstLine) {
|
|
|
6203
6392
|
return null;
|
|
6204
6393
|
const parts = rest.split(/\s+/);
|
|
6205
6394
|
if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
|
|
6206
|
-
return parts[1] && parts[1] !== "-S" ?
|
|
6395
|
+
return parts[1] && parts[1] !== "-S" ? basename3(parts[1]) : parts[2] ? basename3(parts[2]) : null;
|
|
6207
6396
|
}
|
|
6208
|
-
return
|
|
6397
|
+
return basename3(parts[0]);
|
|
6209
6398
|
}
|
|
6210
6399
|
function interpreterFor(name, scriptPath, content) {
|
|
6211
6400
|
const ext = extensionOf(scriptPath);
|
|
@@ -6239,8 +6428,8 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6239
6428
|
result = await Promise.race([
|
|
6240
6429
|
reader.read(),
|
|
6241
6430
|
new Promise((_, reject) => {
|
|
6242
|
-
const
|
|
6243
|
-
|
|
6431
|
+
const t2 = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
|
|
6432
|
+
t2.unref?.();
|
|
6244
6433
|
})
|
|
6245
6434
|
]);
|
|
6246
6435
|
} catch {
|
|
@@ -6258,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6258
6447
|
return chunks.join("");
|
|
6259
6448
|
}
|
|
6260
6449
|
async function executeVerifiedScript(options) {
|
|
6261
|
-
const scriptDir =
|
|
6450
|
+
const scriptDir = dirname9(options.scriptPath);
|
|
6262
6451
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6263
|
-
const tempPath =
|
|
6452
|
+
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6264
6453
|
let proc = null;
|
|
6265
6454
|
let timedOut = false;
|
|
6266
6455
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6269,7 +6458,7 @@ async function executeVerifiedScript(options) {
|
|
|
6269
6458
|
}, options.timeout) : null;
|
|
6270
6459
|
try {
|
|
6271
6460
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
6272
|
-
proc = Bun.spawn([...interpreter.command,
|
|
6461
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
6273
6462
|
stdin: new Response(options.stdin),
|
|
6274
6463
|
stdout: "pipe",
|
|
6275
6464
|
stderr: "pipe",
|
|
@@ -6845,6 +7034,7 @@ var HOOKS = [
|
|
|
6845
7034
|
},
|
|
6846
7035
|
{
|
|
6847
7036
|
name: "workspace-repos-guard",
|
|
7037
|
+
timeoutSeconds: 10,
|
|
6848
7038
|
displayName: "Workspace Repos Guard",
|
|
6849
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",
|
|
6850
7040
|
version: "0.1.0",
|
|
@@ -6863,7 +7053,7 @@ var HOOKS = [
|
|
|
6863
7053
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
6864
7054
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
6865
7055
|
rewritesInput: true,
|
|
6866
|
-
timeoutSeconds:
|
|
7056
|
+
timeoutSeconds: 10
|
|
6867
7057
|
},
|
|
6868
7058
|
{
|
|
6869
7059
|
name: "checktests",
|
|
@@ -7352,9 +7542,9 @@ function getHook(name) {
|
|
|
7352
7542
|
return HOOKS.find((h) => h.name === name);
|
|
7353
7543
|
}
|
|
7354
7544
|
// src/lib/installer.ts
|
|
7355
|
-
import { existsSync as
|
|
7356
|
-
import { join as
|
|
7357
|
-
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";
|
|
7358
7548
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7359
7549
|
|
|
7360
7550
|
// src/lib/resolve.ts
|
|
@@ -7445,6 +7635,137 @@ function resolveHook(name) {
|
|
|
7445
7635
|
// src/lib/installer.ts
|
|
7446
7636
|
init_manifest();
|
|
7447
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
|
+
|
|
7448
7769
|
// src/lib/mementos-options.ts
|
|
7449
7770
|
function validateMementosRegistration(name, value) {
|
|
7450
7771
|
if (value === undefined)
|
|
@@ -7506,6 +7827,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
|
|
|
7506
7827
|
function registrationName(command) {
|
|
7507
7828
|
if (typeof command !== "string")
|
|
7508
7829
|
return;
|
|
7830
|
+
const native = nativeSafetyRegistration(command);
|
|
7831
|
+
if (native)
|
|
7832
|
+
return native;
|
|
7509
7833
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
7510
7834
|
try {
|
|
7511
7835
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -7525,16 +7849,16 @@ function matchersOverlap(a, b) {
|
|
|
7525
7849
|
init_store();
|
|
7526
7850
|
|
|
7527
7851
|
// src/lib/codex-settings.ts
|
|
7528
|
-
import { closeSync, existsSync as
|
|
7529
|
-
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";
|
|
7530
7854
|
import { randomUUID } from "crypto";
|
|
7531
7855
|
var snapshots = new WeakMap;
|
|
7532
7856
|
function current(path) {
|
|
7533
7857
|
try {
|
|
7534
|
-
const stat =
|
|
7858
|
+
const stat = lstatSync2(path);
|
|
7535
7859
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
7536
7860
|
throw new Error("invalid_file");
|
|
7537
|
-
return
|
|
7861
|
+
return readFileSync5(path, "utf8");
|
|
7538
7862
|
} catch (error) {
|
|
7539
7863
|
if (error.code === "ENOENT")
|
|
7540
7864
|
return null;
|
|
@@ -7566,11 +7890,11 @@ function writeCodexSettings(path, settings) {
|
|
|
7566
7890
|
const snapshot = snapshots.get(settings);
|
|
7567
7891
|
if (!snapshot || snapshot.path !== path)
|
|
7568
7892
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
7569
|
-
mkdirSync4(
|
|
7893
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
7570
7894
|
const lock = `${path}.hooks-write.lock`;
|
|
7571
7895
|
let fd;
|
|
7572
7896
|
try {
|
|
7573
|
-
fd =
|
|
7897
|
+
fd = openSync2(lock, "wx", 384);
|
|
7574
7898
|
} catch {
|
|
7575
7899
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
7576
7900
|
}
|
|
@@ -7586,16 +7910,16 @@ function writeCodexSettings(path, settings) {
|
|
|
7586
7910
|
renameSync2(temp, path);
|
|
7587
7911
|
snapshots.set(settings, { path, raw: next });
|
|
7588
7912
|
} finally {
|
|
7589
|
-
if (
|
|
7913
|
+
if (existsSync8(temp))
|
|
7590
7914
|
unlinkSync(temp);
|
|
7591
|
-
|
|
7915
|
+
closeSync2(fd);
|
|
7592
7916
|
unlinkSync(lock);
|
|
7593
7917
|
}
|
|
7594
7918
|
}
|
|
7595
7919
|
|
|
7596
7920
|
// src/lib/installer.ts
|
|
7597
|
-
var __dirname2 =
|
|
7598
|
-
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");
|
|
7599
7923
|
function normalizeHookName(name) {
|
|
7600
7924
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
7601
7925
|
}
|
|
@@ -7607,8 +7931,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
7607
7931
|
if (!Array.isArray(entry.hooks))
|
|
7608
7932
|
return [entry];
|
|
7609
7933
|
const kept = entry.hooks.filter((h) => {
|
|
7610
|
-
|
|
7611
|
-
return !(match && match[1] === hookName);
|
|
7934
|
+
return registrationName(h.command) !== hookName;
|
|
7612
7935
|
});
|
|
7613
7936
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
7614
7937
|
});
|
|
@@ -7684,29 +8007,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
7684
8007
|
return globalOverride;
|
|
7685
8008
|
const dir = getTargetSettingsDir(target);
|
|
7686
8009
|
if (target === "codex")
|
|
7687
|
-
return
|
|
8010
|
+
return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
7688
8011
|
if (scope === "project") {
|
|
7689
|
-
return target === "codewith" ?
|
|
8012
|
+
return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
|
|
7690
8013
|
}
|
|
7691
|
-
return target === "codewith" ?
|
|
8014
|
+
return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
|
|
7692
8015
|
}
|
|
7693
8016
|
function getHookPath(name) {
|
|
7694
8017
|
const shortName = shortHookName(name);
|
|
7695
8018
|
const resolved = resolveHookDir(shortName);
|
|
7696
8019
|
if (resolved)
|
|
7697
8020
|
return resolved;
|
|
7698
|
-
return
|
|
8021
|
+
return join8(HOOKS_DIR, normalizeHookName(shortName));
|
|
7699
8022
|
}
|
|
7700
8023
|
function hookExists(name) {
|
|
7701
|
-
return
|
|
8024
|
+
return existsSync9(getHookPath(name));
|
|
7702
8025
|
}
|
|
7703
8026
|
function readSettings(scope = "global", target = "claude") {
|
|
7704
8027
|
const path = getSettingsPath(scope, target);
|
|
7705
8028
|
if (target === "codex")
|
|
7706
8029
|
return readCodexSettings(path);
|
|
7707
8030
|
try {
|
|
7708
|
-
if (
|
|
7709
|
-
return JSON.parse(
|
|
8031
|
+
if (existsSync9(path)) {
|
|
8032
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
7710
8033
|
}
|
|
7711
8034
|
} catch (error) {
|
|
7712
8035
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -7717,8 +8040,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
7717
8040
|
const path = getSettingsPath(scope, target);
|
|
7718
8041
|
if (target === "codex")
|
|
7719
8042
|
return writeCodexSettings(path, settings);
|
|
7720
|
-
const dir =
|
|
7721
|
-
if (!
|
|
8043
|
+
const dir = dirname5(path);
|
|
8044
|
+
if (!existsSync9(dir)) {
|
|
7722
8045
|
mkdirSync5(dir, { recursive: true });
|
|
7723
8046
|
}
|
|
7724
8047
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -7746,6 +8069,9 @@ function codewithTimeout(name) {
|
|
|
7746
8069
|
switch (name) {
|
|
7747
8070
|
case "mementos-context":
|
|
7748
8071
|
return 12;
|
|
8072
|
+
case "trash-guard":
|
|
8073
|
+
case "workspace-repos-guard":
|
|
8074
|
+
return 10;
|
|
7749
8075
|
case "knowledge-context":
|
|
7750
8076
|
return 6;
|
|
7751
8077
|
case "session-start":
|
|
@@ -7755,7 +8081,6 @@ function codewithTimeout(name) {
|
|
|
7755
8081
|
case "prompt-guard":
|
|
7756
8082
|
return 3;
|
|
7757
8083
|
case "worktree-guard":
|
|
7758
|
-
case "trash-guard":
|
|
7759
8084
|
case "stop-sync":
|
|
7760
8085
|
return 5;
|
|
7761
8086
|
default:
|
|
@@ -7784,11 +8109,13 @@ function codewithStatusMessage(name) {
|
|
|
7784
8109
|
}
|
|
7785
8110
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
7786
8111
|
const shortName = shortHookName(name);
|
|
7787
|
-
|
|
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);
|
|
7788
8115
|
if (!meta)
|
|
7789
8116
|
throw new Error(`Hook '${shortName}' not found`);
|
|
7790
8117
|
validateMementosRegistration(shortName, mementos);
|
|
7791
|
-
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);
|
|
7792
8119
|
const matcher = codewithMatcher(meta.matcher);
|
|
7793
8120
|
const fragments = [];
|
|
7794
8121
|
for (const event of getHookEvents(meta)) {
|
|
@@ -7813,17 +8140,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
7813
8140
|
function readCodewithConfig(scope, configPath) {
|
|
7814
8141
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7815
8142
|
try {
|
|
7816
|
-
return
|
|
8143
|
+
return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
7817
8144
|
} catch {
|
|
7818
8145
|
return "";
|
|
7819
8146
|
}
|
|
7820
8147
|
}
|
|
7821
8148
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
7822
8149
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7823
|
-
const dir =
|
|
7824
|
-
if (!
|
|
8150
|
+
const dir = dirname5(path);
|
|
8151
|
+
if (!existsSync9(dir))
|
|
7825
8152
|
mkdirSync5(dir, { recursive: true });
|
|
7826
|
-
const existing =
|
|
8153
|
+
const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
7827
8154
|
const sep2 = existing.trim() ? `
|
|
7828
8155
|
|
|
7829
8156
|
` : "";
|
|
@@ -7831,7 +8158,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
7831
8158
|
return path;
|
|
7832
8159
|
}
|
|
7833
8160
|
function detectConflict(name, scope, target) {
|
|
7834
|
-
const meta = resolveHookMeta(name);
|
|
8161
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
7835
8162
|
if (!meta || !meta.matcher)
|
|
7836
8163
|
return;
|
|
7837
8164
|
const events = new Set(getHookEvents(meta));
|
|
@@ -7851,7 +8178,7 @@ function detectConflict(name, scope, target) {
|
|
|
7851
8178
|
return;
|
|
7852
8179
|
}
|
|
7853
8180
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
7854
|
-
const meta = resolveHookMeta(name);
|
|
8181
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
7855
8182
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
7856
8183
|
return;
|
|
7857
8184
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -7872,7 +8199,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
7872
8199
|
}
|
|
7873
8200
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
7874
8201
|
const shortName = shortHookName(name);
|
|
7875
|
-
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)) {
|
|
7876
8206
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
7877
8207
|
}
|
|
7878
8208
|
if (target === "codewith") {
|
|
@@ -7964,7 +8294,7 @@ function installHook(name, options = {}) {
|
|
|
7964
8294
|
}
|
|
7965
8295
|
if (target === "all") {
|
|
7966
8296
|
const shortName = shortHookName(name);
|
|
7967
|
-
const meta = resolveHookMeta(shortName);
|
|
8297
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
7968
8298
|
if (meta) {
|
|
7969
8299
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
7970
8300
|
if (unsupportedTargets.length > 0) {
|
|
@@ -7997,7 +8327,7 @@ function installHook(name, options = {}) {
|
|
|
7997
8327
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
7998
8328
|
}
|
|
7999
8329
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
8000
|
-
const meta = resolveHookMeta(name);
|
|
8330
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
8001
8331
|
if (!meta)
|
|
8002
8332
|
return;
|
|
8003
8333
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -8014,8 +8344,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
8014
8344
|
const settings = readSettings(scope, target);
|
|
8015
8345
|
if (!settings.hooks)
|
|
8016
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
|
+
}
|
|
8017
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;
|
|
8018
|
-
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);
|
|
8019
8358
|
if (target === "codex") {
|
|
8020
8359
|
const positions = [];
|
|
8021
8360
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -8023,9 +8362,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
8023
8362
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
8024
8363
|
const command = entry.hooks[index]?.command;
|
|
8025
8364
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
8026
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
8365
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
8027
8366
|
if (registrationName(command) !== name)
|
|
8028
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.");
|
|
8029
8370
|
positions.push({ event, entry, index });
|
|
8030
8371
|
}
|
|
8031
8372
|
}
|
|
@@ -8089,12 +8430,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
8089
8430
|
if (target === "codewith") {
|
|
8090
8431
|
const config = readCodewithConfig(scope);
|
|
8091
8432
|
const registered2 = [];
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
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 [];
|
|
8098
8447
|
}
|
|
8099
8448
|
return [...new Set(registered2)];
|
|
8100
8449
|
}
|
|
@@ -8195,10 +8544,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
8195
8544
|
}
|
|
8196
8545
|
function codewithHasHookEntry(name, scope = "global") {
|
|
8197
8546
|
const path = getSettingsPath(scope, "codewith");
|
|
8198
|
-
if (!
|
|
8547
|
+
if (!existsSync9(path))
|
|
8199
8548
|
return false;
|
|
8200
8549
|
try {
|
|
8201
|
-
return codewithHasHookEntryInText(
|
|
8550
|
+
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
8202
8551
|
} catch {
|
|
8203
8552
|
return false;
|
|
8204
8553
|
}
|
|
@@ -8207,12 +8556,12 @@ function removeStoreDirFailClosed(dir) {
|
|
|
8207
8556
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
8208
8557
|
for (const entry of entries) {
|
|
8209
8558
|
if (entry.isDirectory()) {
|
|
8210
|
-
removeStoreDirFailClosed(
|
|
8559
|
+
removeStoreDirFailClosed(join8(dir, entry.name));
|
|
8211
8560
|
}
|
|
8212
8561
|
}
|
|
8213
8562
|
for (const entry of entries) {
|
|
8214
8563
|
if (!entry.isDirectory()) {
|
|
8215
|
-
unlinkSync2(
|
|
8564
|
+
unlinkSync2(join8(dir, entry.name));
|
|
8216
8565
|
}
|
|
8217
8566
|
}
|
|
8218
8567
|
rmdirSync(dir);
|
|
@@ -8243,8 +8592,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8243
8592
|
const registrationsRemaining = [];
|
|
8244
8593
|
if (target === "codewith" || target === "all") {
|
|
8245
8594
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
8246
|
-
if (
|
|
8247
|
-
const before =
|
|
8595
|
+
if (existsSync9(codewithPath)) {
|
|
8596
|
+
const before = readFileSync6(codewithPath, "utf-8");
|
|
8248
8597
|
const after = removeCodewithHookEntry(before, shortName);
|
|
8249
8598
|
if (after.removed) {
|
|
8250
8599
|
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
@@ -8286,7 +8635,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8286
8635
|
// src/index.ts
|
|
8287
8636
|
init_profiles();
|
|
8288
8637
|
init_config();
|
|
8289
|
-
import { existsSync as
|
|
8638
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
8290
8639
|
|
|
8291
8640
|
// src/lib/transport.ts
|
|
8292
8641
|
init_transport();
|
|
@@ -8321,6 +8670,41 @@ function rethrowHooksAuthorityFailure(error) {
|
|
|
8321
8670
|
}
|
|
8322
8671
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
8323
8672
|
}
|
|
8673
|
+
var authorityBindings = new WeakMap;
|
|
8674
|
+
function sameCredential(left, right) {
|
|
8675
|
+
return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
|
|
8676
|
+
}
|
|
8677
|
+
function changedBinding() {
|
|
8678
|
+
throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
|
|
8679
|
+
}
|
|
8680
|
+
async function resolveHooksRequestAuthority(authority) {
|
|
8681
|
+
if (authority.origin !== hooksRegistryOrigin(authority.v1BaseUrl))
|
|
8682
|
+
changedBinding();
|
|
8683
|
+
const binding = authorityBindings.get(authority);
|
|
8684
|
+
if (!binding)
|
|
8685
|
+
return { ...authority, apiKey: explicitCredential("hooks", authority.apiKey).apiKey };
|
|
8686
|
+
const snapshot = () => {
|
|
8687
|
+
const next = resolveHooksTransport(binding.env, binding.options);
|
|
8688
|
+
if (next.mode !== "remote" || !next.authority || next.authority.v1BaseUrl !== authority.v1BaseUrl)
|
|
8689
|
+
changedBinding();
|
|
8690
|
+
return { authority: next.authority, binding: authorityBindings.get(next.authority) };
|
|
8691
|
+
};
|
|
8692
|
+
const first = snapshot();
|
|
8693
|
+
const reviewed = snapshot();
|
|
8694
|
+
if (!sameCredential(first.binding.credential, reviewed.binding.credential))
|
|
8695
|
+
changedBinding();
|
|
8696
|
+
let credential = reviewed.binding.credential;
|
|
8697
|
+
try {
|
|
8698
|
+
if (credential.tier === "pointer")
|
|
8699
|
+
credential = await completePointerCredential("hooks", credential, binding.env);
|
|
8700
|
+
} catch (error) {
|
|
8701
|
+
rethrowHooksAuthorityFailure(error);
|
|
8702
|
+
}
|
|
8703
|
+
const final = snapshot();
|
|
8704
|
+
if (!sameCredential(reviewed.binding.credential, final.binding.credential))
|
|
8705
|
+
changedBinding();
|
|
8706
|
+
return { ...reviewed.authority, apiKey: credential.apiKey };
|
|
8707
|
+
}
|
|
8324
8708
|
function resolveHooksTransport(env = process.env, options = {}) {
|
|
8325
8709
|
if (selectsHooksLocalStore(env)) {
|
|
8326
8710
|
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
@@ -8334,7 +8718,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
8334
8718
|
try {
|
|
8335
8719
|
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
8336
8720
|
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
8337
|
-
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
8721
|
+
credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
8338
8722
|
});
|
|
8339
8723
|
} catch (error) {
|
|
8340
8724
|
rethrowHooksAuthorityFailure(error);
|
|
@@ -8342,25 +8726,43 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
8342
8726
|
if (!credential) {
|
|
8343
8727
|
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
8344
8728
|
}
|
|
8729
|
+
const authority = {
|
|
8730
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
8731
|
+
apiKey: credential.apiKey,
|
|
8732
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
8733
|
+
apiKeySource: credential.source,
|
|
8734
|
+
apiKeyTier: credential.tier,
|
|
8735
|
+
v1BaseUrl: resolution.baseUrl,
|
|
8736
|
+
warning: resolution.warning
|
|
8737
|
+
};
|
|
8738
|
+
authorityBindings.set(authority, { env, options, credential });
|
|
8345
8739
|
return {
|
|
8346
8740
|
mode: "remote",
|
|
8347
8741
|
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
8348
|
-
authority
|
|
8349
|
-
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
8350
|
-
apiKey: credential.apiKey,
|
|
8351
|
-
apiUrlSource: resolution.apiUrlSource,
|
|
8352
|
-
apiKeySource: credential.source,
|
|
8353
|
-
apiKeyTier: credential.tier,
|
|
8354
|
-
v1BaseUrl: resolution.baseUrl,
|
|
8355
|
-
warning: resolution.warning
|
|
8356
|
-
}
|
|
8742
|
+
authority
|
|
8357
8743
|
};
|
|
8358
8744
|
}
|
|
8359
8745
|
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
8360
8746
|
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8361
8747
|
const tier1 = options.credentials?.apiKey;
|
|
8362
8748
|
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
8363
|
-
|
|
8749
|
+
const credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
8750
|
+
if (credential?.tier === "pointer")
|
|
8751
|
+
throw new Error("Vault references require resolveHooksServePublishKeyAsync().");
|
|
8752
|
+
return credential?.apiKey;
|
|
8753
|
+
}
|
|
8754
|
+
async function resolveHooksServePublishKeyAsync(env = process.env, options = {}) {
|
|
8755
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8756
|
+
const read = () => resolveCredential("hooks", resolverEnv, credentials);
|
|
8757
|
+
const selected = read();
|
|
8758
|
+
if (!selected || selected.tier !== "pointer")
|
|
8759
|
+
return selected?.apiKey;
|
|
8760
|
+
if (!sameCredential(selected, read()))
|
|
8761
|
+
changedBinding();
|
|
8762
|
+
const completed = await completePointerCredential("hooks", selected, env);
|
|
8763
|
+
if (!sameCredential(selected, read()))
|
|
8764
|
+
changedBinding();
|
|
8765
|
+
return completed.apiKey;
|
|
8364
8766
|
}
|
|
8365
8767
|
|
|
8366
8768
|
// src/index.ts
|
|
@@ -8373,15 +8775,15 @@ init_manifest();
|
|
|
8373
8775
|
// src/lib/custom-install.ts
|
|
8374
8776
|
init_config();
|
|
8375
8777
|
init_manifest();
|
|
8376
|
-
import { existsSync as
|
|
8377
|
-
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";
|
|
8378
8780
|
import { tmpdir } from "os";
|
|
8379
8781
|
function manifestAt(dir) {
|
|
8380
|
-
const path =
|
|
8381
|
-
if (!
|
|
8782
|
+
const path = join11(dir, "manifest.json");
|
|
8783
|
+
if (!existsSync11(path))
|
|
8382
8784
|
return;
|
|
8383
8785
|
try {
|
|
8384
|
-
return parseManifest(
|
|
8786
|
+
return parseManifest(readFileSync9(path, "utf-8"));
|
|
8385
8787
|
} catch {
|
|
8386
8788
|
return;
|
|
8387
8789
|
}
|
|
@@ -8400,8 +8802,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
8400
8802
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
8401
8803
|
}
|
|
8402
8804
|
async function cloneToTemp(gitUrl) {
|
|
8403
|
-
const tmp = mkdtempSync(
|
|
8404
|
-
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")], {
|
|
8405
8807
|
stdout: "pipe",
|
|
8406
8808
|
stderr: "pipe"
|
|
8407
8809
|
});
|
|
@@ -8414,7 +8816,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
8414
8816
|
rmSync2(tmp, { recursive: true, force: true });
|
|
8415
8817
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
8416
8818
|
}
|
|
8417
|
-
return
|
|
8819
|
+
return join11(tmp, "repo");
|
|
8418
8820
|
}
|
|
8419
8821
|
function isGitUrl(value) {
|
|
8420
8822
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -8424,7 +8826,7 @@ function isCustomSource(value) {
|
|
|
8424
8826
|
return true;
|
|
8425
8827
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
8426
8828
|
return true;
|
|
8427
|
-
return
|
|
8829
|
+
return existsSync11(value);
|
|
8428
8830
|
}
|
|
8429
8831
|
async function installCustomSource(source) {
|
|
8430
8832
|
if (isGitUrl(source)) {
|
|
@@ -8438,11 +8840,11 @@ async function installCustomSource(source) {
|
|
|
8438
8840
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
8439
8841
|
return installManifestUrl(source);
|
|
8440
8842
|
}
|
|
8441
|
-
if (!
|
|
8843
|
+
if (!existsSync11(source)) {
|
|
8442
8844
|
throw new Error(`No such local path: ${source}`);
|
|
8443
8845
|
}
|
|
8444
8846
|
if (source.endsWith("manifest.json")) {
|
|
8445
|
-
const manifest = parseManifest(
|
|
8847
|
+
const manifest = parseManifest(readFileSync9(source, "utf-8"));
|
|
8446
8848
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
8447
8849
|
}
|
|
8448
8850
|
return copyManifestDir(source, source, "local");
|
|
@@ -8472,16 +8874,16 @@ async function installManifestUrl(url) {
|
|
|
8472
8874
|
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
8473
8875
|
}
|
|
8474
8876
|
// src/lib/sync.ts
|
|
8475
|
-
import { createHash as
|
|
8476
|
-
import { readFileSync as
|
|
8877
|
+
import { createHash as createHash3 } from "crypto";
|
|
8878
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8477
8879
|
init_db();
|
|
8478
8880
|
init_store();
|
|
8479
8881
|
init_manifest();
|
|
8480
8882
|
function sha256OfText(text) {
|
|
8481
|
-
return
|
|
8883
|
+
return createHash3("sha256").update(text).digest("hex");
|
|
8482
8884
|
}
|
|
8483
8885
|
function sha256OfPath(path) {
|
|
8484
|
-
return
|
|
8886
|
+
return createHash3("sha256").update(readFileSync10(path)).digest("hex");
|
|
8485
8887
|
}
|
|
8486
8888
|
function collectBundledCatalog() {
|
|
8487
8889
|
const out = [];
|
|
@@ -8499,7 +8901,14 @@ function collectBundledCatalog() {
|
|
|
8499
8901
|
}
|
|
8500
8902
|
async function fetchJson(base, path, apiKey, opts) {
|
|
8501
8903
|
const headers = { accept: "application/json" };
|
|
8502
|
-
if (apiKey)
|
|
8904
|
+
if (typeof apiKey === "object") {
|
|
8905
|
+
if (apiKey.origin !== base)
|
|
8906
|
+
throw new Error("Hooks registry authority does not match the request target");
|
|
8907
|
+
const resolved = await resolveHooksRequestAuthority(apiKey);
|
|
8908
|
+
if (resolved.origin !== base)
|
|
8909
|
+
throw new Error("Hooks registry authority changed before dispatch");
|
|
8910
|
+
headers["x-api-key"] = resolved.apiKey;
|
|
8911
|
+
} else if (apiKey)
|
|
8503
8912
|
headers["x-api-key"] = apiKey;
|
|
8504
8913
|
const res = await fetch(`${base}${path}`, {
|
|
8505
8914
|
headers,
|
|
@@ -8567,8 +8976,8 @@ async function planSync(options = {}) {
|
|
|
8567
8976
|
credentials: options.credentials
|
|
8568
8977
|
});
|
|
8569
8978
|
if (transport.mode === "remote" && transport.authority) {
|
|
8570
|
-
const { origin
|
|
8571
|
-
const { catalog, remoteLock } = await fetchRemoteState(origin,
|
|
8979
|
+
const { origin } = transport.authority;
|
|
8980
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
|
|
8572
8981
|
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
8573
8982
|
}
|
|
8574
8983
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
@@ -8596,10 +9005,10 @@ async function syncHooks(options = {}) {
|
|
|
8596
9005
|
}
|
|
8597
9006
|
return plan;
|
|
8598
9007
|
}
|
|
8599
|
-
const { origin
|
|
8600
|
-
const { catalog, remoteLock } = await fetchRemoteState(origin,
|
|
9008
|
+
const { origin } = transport.authority;
|
|
9009
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
|
|
8601
9010
|
const diff = computeDiff(catalog, remoteLock);
|
|
8602
|
-
const staged = await stageSyncArtifacts(origin,
|
|
9011
|
+
const staged = await stageSyncArtifacts(origin, transport.authority, [...diff.added, ...diff.updated], remoteLock);
|
|
8603
9012
|
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
8604
9013
|
return { apiUrl: origin, dryRun: false, diff };
|
|
8605
9014
|
}
|
|
@@ -8775,9 +9184,9 @@ async function installPinnedFromBundled(name, version) {
|
|
|
8775
9184
|
// src/serve.ts
|
|
8776
9185
|
init_manifest();
|
|
8777
9186
|
init_store();
|
|
8778
|
-
import { readFileSync as
|
|
8779
|
-
import { dirname as
|
|
8780
|
-
import { fileURLToPath as
|
|
9187
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9188
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
9189
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8781
9190
|
|
|
8782
9191
|
// src/lib/secure-compare.ts
|
|
8783
9192
|
function secureEqual(a, b) {
|
|
@@ -8785,163 +9194,364 @@ function secureEqual(a, b) {
|
|
|
8785
9194
|
const ab = enc.encode(a);
|
|
8786
9195
|
const bb = enc.encode(b);
|
|
8787
9196
|
let diff = ab.length ^ bb.length;
|
|
8788
|
-
const
|
|
8789
|
-
for (let i = 0;i <
|
|
9197
|
+
const n2 = Math.max(ab.length, bb.length);
|
|
9198
|
+
for (let i = 0;i < n2; i++) {
|
|
8790
9199
|
diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
|
|
8791
9200
|
}
|
|
8792
9201
|
return diff === 0;
|
|
8793
9202
|
}
|
|
8794
9203
|
|
|
8795
9204
|
// src/openapi.ts
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
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
|
+
}
|
|
8815
9226
|
}
|
|
8816
9227
|
}
|
|
8817
9228
|
}
|
|
8818
9229
|
}
|
|
8819
|
-
}
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
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
|
+
}
|
|
8828
9253
|
}
|
|
8829
|
-
}
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
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
|
+
}
|
|
8841
9266
|
}
|
|
8842
9267
|
}
|
|
8843
9268
|
}
|
|
8844
9269
|
}
|
|
8845
|
-
}
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
summary: "List the full hook catalog with versions and sha256",
|
|
8857
|
-
operationId: "getCatalog",
|
|
8858
|
-
responses: {
|
|
8859
|
-
"200": {
|
|
8860
|
-
description: "Catalog entries",
|
|
8861
|
-
content: {
|
|
8862
|
-
"application/json": {
|
|
8863
|
-
schema: {
|
|
8864
|
-
type: "object",
|
|
8865
|
-
properties: {
|
|
8866
|
-
hooks: {
|
|
8867
|
-
type: "array",
|
|
8868
|
-
items: {
|
|
8869
|
-
type: "object",
|
|
8870
|
-
properties: {
|
|
8871
|
-
name: { type: "string" },
|
|
8872
|
-
version: { type: "string" },
|
|
8873
|
-
sha256: { type: "string" },
|
|
8874
|
-
events: { type: "array", items: { type: "string" } }
|
|
8875
|
-
}
|
|
8876
|
-
}
|
|
8877
|
-
}
|
|
8878
|
-
}
|
|
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 }
|
|
8879
9281
|
}
|
|
8880
9282
|
}
|
|
8881
9283
|
}
|
|
8882
9284
|
}
|
|
8883
9285
|
}
|
|
8884
|
-
}
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
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
|
+
}
|
|
8904
9372
|
},
|
|
8905
|
-
|
|
8906
|
-
|
|
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
|
+
}
|
|
9414
|
+
},
|
|
9415
|
+
security: [{ apiKey: [] }]
|
|
8907
9416
|
}
|
|
8908
9417
|
}
|
|
8909
9418
|
},
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
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" }
|
|
8922
9491
|
}
|
|
8923
9492
|
}
|
|
8924
9493
|
},
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
"
|
|
8929
|
-
|
|
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
|
+
}
|
|
8930
9508
|
},
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
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
|
+
}
|
|
8941
9551
|
}
|
|
8942
9552
|
}
|
|
8943
|
-
}
|
|
8944
|
-
}
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
8945
9555
|
|
|
8946
9556
|
// src/serve.ts
|
|
8947
9557
|
init_semver();
|
|
@@ -8950,8 +9560,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
8950
9560
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
8951
9561
|
function packageVersion() {
|
|
8952
9562
|
try {
|
|
8953
|
-
const pkgPath =
|
|
8954
|
-
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";
|
|
8955
9565
|
} catch {
|
|
8956
9566
|
return "0.0.0";
|
|
8957
9567
|
}
|
|
@@ -9040,7 +9650,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
9040
9650
|
return Promise.resolve(json({ version: packageVersion() }));
|
|
9041
9651
|
}
|
|
9042
9652
|
if (url.pathname === "/openapi.json" && req.method === "GET") {
|
|
9043
|
-
return Promise.resolve(json(
|
|
9653
|
+
return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
|
|
9044
9654
|
}
|
|
9045
9655
|
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
9046
9656
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
@@ -9112,7 +9722,7 @@ function startServeServer(options) {
|
|
|
9112
9722
|
hostname: host,
|
|
9113
9723
|
port,
|
|
9114
9724
|
async fetch(req) {
|
|
9115
|
-
return handleServeRequest(req,
|
|
9725
|
+
return handleServeRequest(req, await resolveHooksServePublishKeyAsync());
|
|
9116
9726
|
}
|
|
9117
9727
|
});
|
|
9118
9728
|
console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
|
|
@@ -9217,11 +9827,11 @@ async function runHook(name, input, options = {}) {
|
|
|
9217
9827
|
throw new Error(`Hook '${name}' not found`);
|
|
9218
9828
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
9219
9829
|
const resolvedScript = custom2?.scriptPath;
|
|
9220
|
-
const script = resolvedScript ?? (
|
|
9830
|
+
const script = resolvedScript ?? (existsSync13(hookScript) ? hookScript : undefined);
|
|
9221
9831
|
if (!script)
|
|
9222
9832
|
throw new Error(`Hook script not found: ${name}`);
|
|
9223
9833
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
9224
|
-
const content =
|
|
9834
|
+
const content = readFileSync12(script);
|
|
9225
9835
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
9226
9836
|
if (!check.ok) {
|
|
9227
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.`);
|
|
@@ -9334,7 +9944,9 @@ export {
|
|
|
9334
9944
|
resolveScriptPath,
|
|
9335
9945
|
resolveScript,
|
|
9336
9946
|
resolveHooksTransport,
|
|
9947
|
+
resolveHooksServePublishKeyAsync,
|
|
9337
9948
|
resolveHooksServePublishKey,
|
|
9949
|
+
resolveHooksRequestAuthority,
|
|
9338
9950
|
resolveHookMeta,
|
|
9339
9951
|
resolveHookEventSink,
|
|
9340
9952
|
resolveHookDir,
|