@hasna/hooks 0.7.10 → 0.8.0
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 +32 -6
- package/bin/index.js +1872 -1300
- package/bin/serve.js +730 -551
- package/dist/config.d.ts +25 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1198 -712
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/installer.d.ts +0 -9
- package/dist/lib/local-opt-in.d.ts +80 -0
- package/dist/lib/profiles.d.ts +10 -0
- package/dist/lib/resolver-types.d.ts +45 -0
- package/dist/lib/sync.d.ts +13 -8
- package/dist/lib/transport.d.ts +87 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/storage.js +54 -63
- package/hooks/hook-affected-tests/package.json +1 -1
- package/hooks/hook-agent-rules-version-check/package.json +1 -1
- package/hooks/hook-agentmessages/package.json +1 -1
- package/hooks/hook-announce-start/package.json +1 -1
- package/hooks/hook-announce-stop/package.json +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-branchprotect/package.json +1 -1
- package/hooks/hook-checkbugs/package.json +1 -1
- package/hooks/hook-checkdocs/package.json +1 -1
- package/hooks/hook-checkfiles/package.json +1 -1
- package/hooks/hook-checklint/package.json +1 -1
- package/hooks/hook-checkpoint/package.json +1 -1
- package/hooks/hook-checksecurity/package.json +1 -1
- package/hooks/hook-checktasks/package.json +1 -1
- package/hooks/hook-checktests/package.json +1 -1
- package/hooks/hook-conflict-detect/package.json +1 -1
- package/hooks/hook-contextrefresh/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-dm-inject/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-failure-to-task/package.json +1 -1
- package/hooks/hook-filelock/package.json +1 -1
- package/hooks/hook-fleet-blockers-gate/package.json +1 -1
- package/hooks/hook-fleet-catchup/package.json +1 -1
- package/hooks/hook-gitguard/package.json +1 -1
- package/hooks/hook-packageage/package.json +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-phonenotify/package.json +1 -1
- package/hooks/hook-precompact/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-stylescheck/package.json +1 -1
- package/hooks/hook-typecheck-gate/package.json +1 -1
- package/package.json +4 -9
- package/scripts/ensure-profiles-dir.mjs +52 -1
package/bin/index.js
CHANGED
|
@@ -1120,8 +1120,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1120
1120
|
args = args.slice();
|
|
1121
1121
|
let launchWithNode = false;
|
|
1122
1122
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1123
|
-
function findFile(
|
|
1124
|
-
const localBin = path.resolve(
|
|
1123
|
+
function findFile(baseDir, baseName) {
|
|
1124
|
+
const localBin = path.resolve(baseDir, baseName);
|
|
1125
1125
|
if (fs.existsSync(localBin))
|
|
1126
1126
|
return localBin;
|
|
1127
1127
|
if (sourceExt.includes(path.extname(baseName)))
|
|
@@ -1886,92 +1886,77 @@ var require_commander = __commonJS((exports) => {
|
|
|
1886
1886
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
1887
1887
|
});
|
|
1888
1888
|
|
|
1889
|
-
//
|
|
1890
|
-
import {
|
|
1891
|
-
import {
|
|
1892
|
-
|
|
1889
|
+
// src/lib/app-home.ts
|
|
1890
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1891
|
+
import { homedir as homedir2 } from "os";
|
|
1892
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1893
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
1894
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
1895
|
+
function pathsResolverAssertApp2(app) {
|
|
1893
1896
|
if (typeof app !== "string" || app.length === 0) {
|
|
1894
1897
|
throw new TypeError("paths: app must be a non-empty string");
|
|
1895
1898
|
}
|
|
1896
|
-
if (!
|
|
1899
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
1897
1900
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
1898
1901
|
}
|
|
1899
1902
|
}
|
|
1900
|
-
function
|
|
1901
|
-
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
const value = envOf2(options)[KIND_ENV2[kind]];
|
|
1905
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1906
|
-
}
|
|
1907
|
-
function isMacOS2(platform) {
|
|
1908
|
-
return platform === "darwin";
|
|
1903
|
+
function pathsResolverAssertKind2(kind) {
|
|
1904
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
1905
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
1906
|
+
}
|
|
1909
1907
|
}
|
|
1910
|
-
function
|
|
1911
|
-
|
|
1912
|
-
|
|
1908
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
1909
|
+
pathsResolverAssertKind2(kind);
|
|
1910
|
+
const env = options.env ?? process.env;
|
|
1911
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
1912
|
+
if (typeof override === "string" && override.length > 0)
|
|
1913
1913
|
return override;
|
|
1914
|
-
const home = options.home ??
|
|
1914
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
1915
1915
|
const platform = options.platform ?? process.platform;
|
|
1916
|
-
if (
|
|
1916
|
+
if (platform === "darwin") {
|
|
1917
1917
|
switch (kind) {
|
|
1918
1918
|
case "config":
|
|
1919
1919
|
case "data":
|
|
1920
|
-
return
|
|
1920
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
1921
1921
|
case "cache":
|
|
1922
|
-
return
|
|
1922
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
1923
1923
|
case "state":
|
|
1924
|
-
return
|
|
1924
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
1925
1925
|
}
|
|
1926
1926
|
}
|
|
1927
1927
|
switch (kind) {
|
|
1928
1928
|
case "config":
|
|
1929
|
-
return
|
|
1929
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
1930
1930
|
case "data":
|
|
1931
|
-
return
|
|
1931
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
1932
1932
|
case "state":
|
|
1933
|
-
return
|
|
1933
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
1934
1934
|
case "cache":
|
|
1935
|
-
return
|
|
1935
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
1936
1936
|
}
|
|
1937
1937
|
}
|
|
1938
|
-
function
|
|
1939
|
-
|
|
1940
|
-
const appSegment = options.internal === true ?
|
|
1941
|
-
return
|
|
1938
|
+
function pathsResolverResolve2(kind, options) {
|
|
1939
|
+
pathsResolverAssertApp2(options.app);
|
|
1940
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
1941
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
1942
1942
|
}
|
|
1943
1943
|
function dataDir2(options) {
|
|
1944
|
-
return
|
|
1944
|
+
return pathsResolverResolve2("data", options);
|
|
1945
1945
|
}
|
|
1946
|
-
var KIND_ENV2, APP_SLUG_RE2;
|
|
1947
|
-
var init_dist = __esm(() => {
|
|
1948
|
-
KIND_ENV2 = {
|
|
1949
|
-
config: "HASNA_CONFIG_HOME",
|
|
1950
|
-
data: "HASNA_DATA_HOME",
|
|
1951
|
-
state: "HASNA_STATE_HOME",
|
|
1952
|
-
cache: "HASNA_CACHE_HOME"
|
|
1953
|
-
};
|
|
1954
|
-
APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1955
|
-
});
|
|
1956
|
-
|
|
1957
|
-
// src/lib/app-home.ts
|
|
1958
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1959
|
-
import { homedir as homedir5 } from "os";
|
|
1960
|
-
import { join as join6, resolve as resolve2 } from "path";
|
|
1961
1946
|
function getHomeDir(env = process.env) {
|
|
1962
|
-
return env.HOME || env.USERPROFILE ||
|
|
1947
|
+
return env.HOME || env.USERPROFILE || homedir2();
|
|
1963
1948
|
}
|
|
1964
1949
|
function getResolverDataRoot(env = process.env) {
|
|
1965
1950
|
return dataDir2({ app: "hooks", home: getHomeDir(env), env });
|
|
1966
1951
|
}
|
|
1967
1952
|
function getLegacyDataRoot(env = process.env) {
|
|
1968
|
-
return
|
|
1953
|
+
return join3(getHomeDir(env), ".hasna", "hooks");
|
|
1969
1954
|
}
|
|
1970
1955
|
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
1971
1956
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
1972
1957
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
1973
1958
|
return true;
|
|
1974
|
-
return existsSync3(
|
|
1959
|
+
return existsSync3(join3(resolved, STORE_MARKER));
|
|
1975
1960
|
}
|
|
1976
1961
|
function getExplicitDataDir(env = process.env) {
|
|
1977
1962
|
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
@@ -1999,7 +1984,7 @@ function getReportedDbPath(env = process.env) {
|
|
|
1999
1984
|
const explicit = getExplicitDbPath(env);
|
|
2000
1985
|
if (explicit)
|
|
2001
1986
|
return explicit;
|
|
2002
|
-
return
|
|
1987
|
+
return join3(getEffectiveDataRoot(env), "hooks.db");
|
|
2003
1988
|
}
|
|
2004
1989
|
function getExplicitDbPath(env = process.env) {
|
|
2005
1990
|
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
@@ -2007,9 +1992,15 @@ function getExplicitDbPath(env = process.env) {
|
|
|
2007
1992
|
return db.trim();
|
|
2008
1993
|
return;
|
|
2009
1994
|
}
|
|
2010
|
-
var STORE_MARKER = "hooks.db";
|
|
1995
|
+
var PATHS_RESOLVER_KIND_ENV2, PATHS_RESOLVER_APP_SLUG_RE2, STORE_MARKER = "hooks.db";
|
|
2011
1996
|
var init_app_home = __esm(() => {
|
|
2012
|
-
|
|
1997
|
+
PATHS_RESOLVER_KIND_ENV2 = {
|
|
1998
|
+
config: "HASNA_CONFIG_HOME",
|
|
1999
|
+
data: "HASNA_DATA_HOME",
|
|
2000
|
+
state: "HASNA_STATE_HOME",
|
|
2001
|
+
cache: "HASNA_CACHE_HOME"
|
|
2002
|
+
};
|
|
2003
|
+
PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2013
2004
|
});
|
|
2014
2005
|
|
|
2015
2006
|
// src/lib/registry.ts
|
|
@@ -4196,690 +4187,294 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4196
4187
|
module.exports = spinners;
|
|
4197
4188
|
});
|
|
4198
4189
|
|
|
4199
|
-
//
|
|
4200
|
-
import {
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
function envToken(name) {
|
|
4204
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
4205
|
-
}
|
|
4206
|
-
function clientTransportEnvKeys(name) {
|
|
4207
|
-
const envSegment = envToken(name);
|
|
4208
|
-
return {
|
|
4209
|
-
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4210
|
-
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4211
|
-
};
|
|
4190
|
+
// src/config.ts
|
|
4191
|
+
import { join as join5 } from "path";
|
|
4192
|
+
function getHooksDataDir() {
|
|
4193
|
+
return getEffectiveDataRoot();
|
|
4212
4194
|
}
|
|
4213
|
-
function
|
|
4214
|
-
return
|
|
4195
|
+
function getCustomHooksDir() {
|
|
4196
|
+
return join5(getHooksDataDir(), "hooks");
|
|
4215
4197
|
}
|
|
4216
|
-
function
|
|
4217
|
-
|
|
4198
|
+
function getLockPath() {
|
|
4199
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4200
|
+
if (explicit)
|
|
4201
|
+
return explicit;
|
|
4202
|
+
return join5(getHooksDataDir(), "hooks.lock");
|
|
4218
4203
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4204
|
+
var init_config = __esm(() => {
|
|
4205
|
+
init_app_home();
|
|
4206
|
+
});
|
|
4207
|
+
|
|
4208
|
+
// src/lib/semver.ts
|
|
4209
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4210
|
+
var init_semver = __esm(() => {
|
|
4211
|
+
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4212
|
+
});
|
|
4213
|
+
|
|
4214
|
+
// src/lib/manifest.ts
|
|
4215
|
+
var exports_manifest = {};
|
|
4216
|
+
__export(exports_manifest, {
|
|
4217
|
+
writeCustomHook: () => writeCustomHook,
|
|
4218
|
+
shortManifestName: () => shortManifestName,
|
|
4219
|
+
scriptRelFor: () => scriptRelFor,
|
|
4220
|
+
resolveScript: () => resolveScript,
|
|
4221
|
+
readCustomManifest: () => readCustomManifest,
|
|
4222
|
+
parseManifest: () => parseManifest,
|
|
4223
|
+
normalizeManifestName: () => normalizeManifestName,
|
|
4224
|
+
manifestSchema: () => manifestSchema,
|
|
4225
|
+
listCustomHooks: () => listCustomHooks,
|
|
4226
|
+
isInlineScript: () => isInlineScript,
|
|
4227
|
+
customHookDir: () => customHookDir,
|
|
4228
|
+
assertContained: () => assertContained,
|
|
4229
|
+
ScriptContainmentError: () => ScriptContainmentError,
|
|
4230
|
+
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4231
|
+
});
|
|
4232
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
4233
|
+
import { basename, dirname, isAbsolute, join as join6, normalize, resolve as resolve3, sep } from "path";
|
|
4234
|
+
import { z } from "zod";
|
|
4235
|
+
function parseManifest(json) {
|
|
4236
|
+
const parsed = JSON.parse(json);
|
|
4237
|
+
return manifestSchema.parse(parsed);
|
|
4222
4238
|
}
|
|
4223
|
-
function
|
|
4224
|
-
|
|
4225
|
-
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4226
|
-
return [];
|
|
4227
|
-
const stem = profile ? `${name}.${profile}` : name;
|
|
4228
|
-
const configStem = profile ? `${name}-${profile}` : name;
|
|
4229
|
-
return [
|
|
4230
|
-
{
|
|
4231
|
-
path: join7(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4232
|
-
tier: "fleet-env",
|
|
4233
|
-
deprecated: false
|
|
4234
|
-
},
|
|
4235
|
-
{
|
|
4236
|
-
path: join7(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
|
|
4237
|
-
tier: "legacy-cloud",
|
|
4238
|
-
deprecated: true
|
|
4239
|
-
},
|
|
4240
|
-
{
|
|
4241
|
-
path: join7(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
|
|
4242
|
-
tier: "config",
|
|
4243
|
-
deprecated: false
|
|
4244
|
-
},
|
|
4245
|
-
{
|
|
4246
|
-
path: join7(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
|
|
4247
|
-
tier: "config-legacy",
|
|
4248
|
-
deprecated: true
|
|
4249
|
-
}
|
|
4250
|
-
];
|
|
4239
|
+
function normalizeManifestName(name) {
|
|
4240
|
+
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4251
4241
|
}
|
|
4252
|
-
function
|
|
4253
|
-
return
|
|
4242
|
+
function shortManifestName(name) {
|
|
4243
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4254
4244
|
}
|
|
4255
|
-
function
|
|
4256
|
-
return
|
|
4245
|
+
function isWithin(child, parent) {
|
|
4246
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4257
4247
|
}
|
|
4258
|
-
function
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
continue;
|
|
4264
|
-
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
4265
|
-
const equals = withoutExport.indexOf("=");
|
|
4266
|
-
if (equals <= 0)
|
|
4267
|
-
continue;
|
|
4268
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
4269
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4270
|
-
continue;
|
|
4271
|
-
let value = withoutExport.slice(equals + 1).trim();
|
|
4272
|
-
const quote = value[0];
|
|
4273
|
-
if (quote === '"' || quote === "'") {
|
|
4274
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
4275
|
-
continue;
|
|
4276
|
-
value = value.slice(1, -1);
|
|
4277
|
-
}
|
|
4278
|
-
if (value.length === 0)
|
|
4279
|
-
continue;
|
|
4280
|
-
values.set(key, value);
|
|
4248
|
+
function assertContainedLexical(target, root, raw) {
|
|
4249
|
+
const lexical = resolve3(target);
|
|
4250
|
+
const rootLexical = resolve3(root);
|
|
4251
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4252
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4281
4253
|
}
|
|
4282
|
-
return
|
|
4254
|
+
return lexical;
|
|
4283
4255
|
}
|
|
4284
|
-
function
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4256
|
+
function realpathNearest(path) {
|
|
4257
|
+
const tail = [];
|
|
4258
|
+
let cur = path;
|
|
4259
|
+
while (!existsSync5(cur)) {
|
|
4260
|
+
const parent = dirname(cur);
|
|
4261
|
+
if (parent === cur)
|
|
4262
|
+
break;
|
|
4263
|
+
tail.unshift(basename(cur));
|
|
4264
|
+
cur = parent;
|
|
4293
4265
|
}
|
|
4294
|
-
return
|
|
4266
|
+
return join6(realpathSync(cur), ...tail);
|
|
4295
4267
|
}
|
|
4296
|
-
function
|
|
4297
|
-
const
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
const value = values.get(key)?.trim();
|
|
4302
|
-
if (value)
|
|
4303
|
-
return value;
|
|
4268
|
+
function assertContainedReal(root, target, raw) {
|
|
4269
|
+
const rootReal = realpathSync(root);
|
|
4270
|
+
const targetReal = realpathNearest(target);
|
|
4271
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4272
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4304
4273
|
}
|
|
4305
|
-
return null;
|
|
4306
4274
|
}
|
|
4307
|
-
function
|
|
4308
|
-
|
|
4309
|
-
|
|
4275
|
+
function assertContained(target, root, raw) {
|
|
4276
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4277
|
+
assertContainedReal(root, lexical, raw);
|
|
4278
|
+
return lexical;
|
|
4279
|
+
}
|
|
4280
|
+
function isInlineScript(manifest) {
|
|
4281
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4282
|
+
`);
|
|
4283
|
+
}
|
|
4284
|
+
function scriptRelFor(manifest) {
|
|
4285
|
+
if (isInlineScript(manifest)) {
|
|
4286
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4287
|
+
return `script${ext}`;
|
|
4310
4288
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4289
|
+
const rel = normalize(manifest.script);
|
|
4290
|
+
if (isAbsolute(rel)) {
|
|
4291
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4292
|
+
}
|
|
4293
|
+
return rel;
|
|
4314
4294
|
}
|
|
4315
|
-
function
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
4323
|
-
warning: fields.warning
|
|
4324
|
-
};
|
|
4325
|
-
const sealed = { ...visible };
|
|
4326
|
-
Object.defineProperty(sealed, "apiKey", {
|
|
4327
|
-
value: apiKey,
|
|
4328
|
-
enumerable: false,
|
|
4329
|
-
writable: false,
|
|
4330
|
-
configurable: false
|
|
4331
|
-
});
|
|
4332
|
-
if (fields.pointerVaultKey !== undefined) {
|
|
4333
|
-
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
4334
|
-
value: fields.pointerVaultKey,
|
|
4335
|
-
enumerable: false,
|
|
4336
|
-
writable: false,
|
|
4337
|
-
configurable: false
|
|
4338
|
-
});
|
|
4295
|
+
function resolveScript(manifest, manifestDir) {
|
|
4296
|
+
if (isInlineScript(manifest))
|
|
4297
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4298
|
+
const rel = scriptRelFor(manifest);
|
|
4299
|
+
const resolved = assertContained(join6(manifestDir, rel), manifestDir, manifest.script);
|
|
4300
|
+
if (!existsSync5(resolved)) {
|
|
4301
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4339
4302
|
}
|
|
4340
|
-
|
|
4341
|
-
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4342
|
-
enumerable: false,
|
|
4343
|
-
writable: false,
|
|
4344
|
-
configurable: false
|
|
4345
|
-
});
|
|
4346
|
-
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4347
|
-
value: true,
|
|
4348
|
-
enumerable: false,
|
|
4349
|
-
writable: false,
|
|
4350
|
-
configurable: false
|
|
4351
|
-
});
|
|
4352
|
-
return Object.freeze(sealed);
|
|
4303
|
+
return { path: rel, content: readFileSync(resolved, "utf-8") };
|
|
4353
4304
|
}
|
|
4354
|
-
function
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4305
|
+
function customHookDir(name) {
|
|
4306
|
+
return join6(getCustomHooksDir(), shortManifestName(name));
|
|
4307
|
+
}
|
|
4308
|
+
function readCustomManifest(name) {
|
|
4309
|
+
const dir = customHookDir(name);
|
|
4310
|
+
const manifestPath = join6(dir, "manifest.json");
|
|
4311
|
+
if (!existsSync5(manifestPath))
|
|
4312
|
+
return;
|
|
4313
|
+
try {
|
|
4314
|
+
const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
|
|
4315
|
+
const script = resolveScript(manifest, dir);
|
|
4316
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4317
|
+
return { manifest, scriptPath: join6(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4318
|
+
} catch (err) {
|
|
4319
|
+
if (err instanceof ScriptContainmentError)
|
|
4320
|
+
throw err;
|
|
4321
|
+
return;
|
|
4359
4322
|
}
|
|
4360
|
-
return null;
|
|
4361
4323
|
}
|
|
4362
|
-
function
|
|
4363
|
-
const
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
const
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
function defaultDeprecationSink(message) {
|
|
4372
|
-
if (typeof process !== "undefined" && process.stderr) {
|
|
4373
|
-
process.stderr.write(`${message}
|
|
4374
|
-
`);
|
|
4324
|
+
function listCustomHooks() {
|
|
4325
|
+
const dir = getCustomHooksDir();
|
|
4326
|
+
if (!existsSync5(dir))
|
|
4327
|
+
return [];
|
|
4328
|
+
const results = [];
|
|
4329
|
+
for (const entry of readdirSafe(dir)) {
|
|
4330
|
+
const parsed = readCustomManifest(entry);
|
|
4331
|
+
if (parsed)
|
|
4332
|
+
results.push(parsed);
|
|
4375
4333
|
}
|
|
4334
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4376
4335
|
}
|
|
4377
|
-
function
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
if (explicitKey) {
|
|
4382
|
-
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4383
|
-
return sealCredential({
|
|
4384
|
-
apiKey: explicitKey,
|
|
4385
|
-
tier: "argument",
|
|
4386
|
-
source: "explicit apiKey argument",
|
|
4387
|
-
deliberate: true,
|
|
4388
|
-
deprecated: false,
|
|
4389
|
-
diskCandidates: diskPaths,
|
|
4390
|
-
warning: null
|
|
4391
|
-
});
|
|
4392
|
-
}
|
|
4393
|
-
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4394
|
-
const overrideRaw = env[overrideKeyName];
|
|
4395
|
-
if (overrideRaw !== undefined) {
|
|
4396
|
-
const override = overrideRaw.trim();
|
|
4397
|
-
if (!override) {
|
|
4398
|
-
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
4399
|
-
}
|
|
4400
|
-
assertUsableCredential(name, overrideKeyName, override);
|
|
4401
|
-
return sealCredential({
|
|
4402
|
-
apiKey: override,
|
|
4403
|
-
tier: "override",
|
|
4404
|
-
source: overrideKeyName,
|
|
4405
|
-
deliberate: true,
|
|
4406
|
-
deprecated: false,
|
|
4407
|
-
diskCandidates: diskPaths,
|
|
4408
|
-
warning: null
|
|
4409
|
-
});
|
|
4410
|
-
}
|
|
4411
|
-
const pointerKeyName = credentialPointerEnvKey(name);
|
|
4412
|
-
const pointerRaw = env[pointerKeyName];
|
|
4413
|
-
if (pointerRaw !== undefined) {
|
|
4414
|
-
const pointer = pointerRaw.trim();
|
|
4415
|
-
if (!pointer) {
|
|
4416
|
-
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
4417
|
-
}
|
|
4418
|
-
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
4419
|
-
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
4420
|
-
}
|
|
4421
|
-
return sealCredential({
|
|
4422
|
-
apiKey: "",
|
|
4423
|
-
pointerVaultKey: pointer,
|
|
4424
|
-
tier: "pointer",
|
|
4425
|
-
source: pointerKeyName,
|
|
4426
|
-
deliberate: true,
|
|
4427
|
-
deprecated: false,
|
|
4428
|
-
diskCandidates: diskPaths,
|
|
4429
|
-
warning: null
|
|
4430
|
-
});
|
|
4431
|
-
}
|
|
4432
|
-
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4433
|
-
if (profile) {
|
|
4434
|
-
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4435
|
-
if (!SAFE_PROFILE.test(profile)) {
|
|
4436
|
-
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4437
|
-
}
|
|
4438
|
-
const paths = profileDiskSources(name, env, profile);
|
|
4439
|
-
for (const path of paths) {
|
|
4440
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
4441
|
-
if (value) {
|
|
4442
|
-
assertUsableCredential(name, path, value);
|
|
4443
|
-
return sealCredential({
|
|
4444
|
-
apiKey: value,
|
|
4445
|
-
tier: "profile",
|
|
4446
|
-
source: path,
|
|
4447
|
-
deliberate: true,
|
|
4448
|
-
deprecated: false,
|
|
4449
|
-
diskCandidates: paths,
|
|
4450
|
-
warning: null
|
|
4451
|
-
});
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
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);
|
|
4455
|
-
}
|
|
4456
|
-
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
4457
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4458
|
-
if (diskHits.length > 0) {
|
|
4459
|
-
const winner = diskHits[0];
|
|
4460
|
-
assertUsableCredential(name, winner.src.path, winner.value);
|
|
4461
|
-
const divergentSources = [
|
|
4462
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
4463
|
-
...(() => {
|
|
4464
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4465
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4466
|
-
})()
|
|
4467
|
-
];
|
|
4468
|
-
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;
|
|
4469
|
-
let deprecated = winner.src.deprecated;
|
|
4470
|
-
let finalWarning = warning;
|
|
4471
|
-
if (winner.src.deprecated) {
|
|
4472
|
-
deprecated = true;
|
|
4473
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4474
|
-
const notified = deprecationNotified();
|
|
4475
|
-
const noticeKey = `${name}:${winner.src.path}`;
|
|
4476
|
-
if (!notified.has(noticeKey)) {
|
|
4477
|
-
notified.add(noticeKey);
|
|
4478
|
-
const target = diskSourceList[0]?.path ?? "<none>";
|
|
4479
|
-
const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
|
|
4480
|
-
sink(message);
|
|
4481
|
-
}
|
|
4482
|
-
finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
|
|
4483
|
-
}
|
|
4484
|
-
return sealCredential({
|
|
4485
|
-
apiKey: winner.value,
|
|
4486
|
-
tier: winner.src.tier,
|
|
4487
|
-
source: winner.src.path,
|
|
4488
|
-
deliberate: false,
|
|
4489
|
-
deprecated,
|
|
4490
|
-
diskCandidates: diskPaths,
|
|
4491
|
-
warning: finalWarning
|
|
4492
|
-
});
|
|
4336
|
+
function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
4337
|
+
const dir = customHookDir(name);
|
|
4338
|
+
if (isAbsolute(scriptRelPath)) {
|
|
4339
|
+
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4493
4340
|
}
|
|
4494
|
-
const
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
diskCandidates: diskPaths,
|
|
4512
|
-
warning: message
|
|
4513
|
-
});
|
|
4341
|
+
const scriptPath = scriptRelPath.includes("/") ? join6(dir, scriptRelPath) : join6(dir, scriptRelPath);
|
|
4342
|
+
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4343
|
+
mkdirSync(dir, { recursive: true });
|
|
4344
|
+
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4345
|
+
const scriptDir = dirname(scriptPath);
|
|
4346
|
+
if (scriptDir !== dir)
|
|
4347
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
4348
|
+
writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
4349
|
+
writeFileSync(join6(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4350
|
+
`, "utf-8");
|
|
4351
|
+
return { dir, scriptPath };
|
|
4352
|
+
}
|
|
4353
|
+
function readdirSafe(dir) {
|
|
4354
|
+
try {
|
|
4355
|
+
return readdirSync(dir);
|
|
4356
|
+
} catch {
|
|
4357
|
+
return [];
|
|
4514
4358
|
}
|
|
4515
|
-
return null;
|
|
4516
4359
|
}
|
|
4517
|
-
var
|
|
4518
|
-
var
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4360
|
+
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4361
|
+
var init_manifest = __esm(() => {
|
|
4362
|
+
init_config();
|
|
4363
|
+
init_semver();
|
|
4364
|
+
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4365
|
+
manifestSchema = z.object({
|
|
4366
|
+
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4367
|
+
version: z.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4368
|
+
description: z.string().optional(),
|
|
4369
|
+
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4370
|
+
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4371
|
+
script_kind: z.enum(["inline", "file"]).optional(),
|
|
4372
|
+
args: z.array(z.string()).optional(),
|
|
4373
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
4374
|
+
env: z.record(z.string(), z.string()).optional()
|
|
4375
|
+
});
|
|
4376
|
+
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4377
|
+
constructor(path, hookDir, raw) {
|
|
4378
|
+
super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
|
|
4379
|
+
this.name = "ScriptContainmentError";
|
|
4527
4380
|
}
|
|
4528
4381
|
};
|
|
4529
|
-
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4530
|
-
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4531
|
-
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4532
|
-
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4533
|
-
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
4534
|
-
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4535
|
-
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4536
|
-
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4537
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
4538
|
-
requireSecretsSdk = createRequire(import.meta.url);
|
|
4539
|
-
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4540
|
-
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4541
|
-
"host",
|
|
4542
|
-
":authority",
|
|
4543
|
-
"forwarded",
|
|
4544
|
-
"x-forwarded-host",
|
|
4545
|
-
"x-original-host"
|
|
4546
|
-
]);
|
|
4547
4382
|
});
|
|
4548
4383
|
|
|
4549
|
-
// src/
|
|
4550
|
-
var
|
|
4551
|
-
__export(
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
getHooksDataDir: () => getHooksDataDir,
|
|
4558
|
-
getCustomHooksDir: () => getCustomHooksDir,
|
|
4559
|
-
getConfigPath: () => getConfigPath
|
|
4384
|
+
// src/lib/resolve.ts
|
|
4385
|
+
var exports_resolve = {};
|
|
4386
|
+
__export(exports_resolve, {
|
|
4387
|
+
resolveScriptPath: () => resolveScriptPath,
|
|
4388
|
+
resolveHookMeta: () => resolveHookMeta,
|
|
4389
|
+
resolveHookDir: () => resolveHookDir,
|
|
4390
|
+
resolveHook: () => resolveHook,
|
|
4391
|
+
resolveBundledHooksDir: () => resolveBundledHooksDir
|
|
4560
4392
|
});
|
|
4561
|
-
import {
|
|
4562
|
-
import {
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4393
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
4394
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4395
|
+
import { fileURLToPath } from "url";
|
|
4396
|
+
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
4397
|
+
const sourceLayout = join7(moduleDir, "..", "..", "hooks");
|
|
4398
|
+
if (existsSync6(join7(sourceLayout, "hook-gitguard")))
|
|
4399
|
+
return sourceLayout;
|
|
4400
|
+
return join7(moduleDir, "..", "hooks");
|
|
4568
4401
|
}
|
|
4569
|
-
function
|
|
4570
|
-
const
|
|
4571
|
-
if (
|
|
4572
|
-
return
|
|
4573
|
-
|
|
4402
|
+
function resolveHookDir(name) {
|
|
4403
|
+
const custom = readCustomManifest(name);
|
|
4404
|
+
if (custom)
|
|
4405
|
+
return customHookDir(name);
|
|
4406
|
+
const meta = getHook(shortManifestName(name));
|
|
4407
|
+
if (!meta)
|
|
4408
|
+
return;
|
|
4409
|
+
const short = shortManifestName(name);
|
|
4410
|
+
const bundledBase = resolveBundledHooksDir();
|
|
4411
|
+
const direct = join7(bundledBase, short);
|
|
4412
|
+
if (existsSync6(direct))
|
|
4413
|
+
return direct;
|
|
4414
|
+
const prefixed = join7(bundledBase, `hook-${short}`);
|
|
4415
|
+
return existsSync6(prefixed) ? prefixed : undefined;
|
|
4574
4416
|
}
|
|
4575
|
-
function
|
|
4576
|
-
const
|
|
4577
|
-
if (
|
|
4578
|
-
return
|
|
4579
|
-
|
|
4417
|
+
function resolveScriptPath(name) {
|
|
4418
|
+
const custom = readCustomManifest(name);
|
|
4419
|
+
if (custom)
|
|
4420
|
+
return custom.scriptPath;
|
|
4421
|
+
const dir = resolveHookDir(name);
|
|
4422
|
+
if (!dir)
|
|
4423
|
+
return;
|
|
4424
|
+
const script = join7(dir, "src", "hook.ts");
|
|
4425
|
+
return existsSync6(script) ? script : undefined;
|
|
4580
4426
|
}
|
|
4581
|
-
function
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4427
|
+
function resolveHookMeta(name) {
|
|
4428
|
+
const custom = readCustomManifest(name);
|
|
4429
|
+
if (custom) {
|
|
4430
|
+
const { manifest } = custom;
|
|
4431
|
+
return {
|
|
4432
|
+
name: shortManifestName(manifest.name),
|
|
4433
|
+
displayName: manifest.description ? manifest.name : manifest.name,
|
|
4434
|
+
description: manifest.description ?? "Custom hook",
|
|
4435
|
+
version: manifest.version,
|
|
4436
|
+
category: "Workflow Automation",
|
|
4437
|
+
event: manifest.events[0] ?? "PostToolUse",
|
|
4438
|
+
events: manifest.events,
|
|
4439
|
+
matcher: "",
|
|
4440
|
+
tags: ["custom"]
|
|
4441
|
+
};
|
|
4589
4442
|
}
|
|
4443
|
+
return getHook(shortManifestName(name));
|
|
4590
4444
|
}
|
|
4591
|
-
function
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
|
|
4445
|
+
function resolveHook(name) {
|
|
4446
|
+
const custom = readCustomManifest(name);
|
|
4447
|
+
if (custom) {
|
|
4448
|
+
const meta2 = resolveHookMeta(name);
|
|
4449
|
+
return {
|
|
4450
|
+
name: shortManifestName(custom.manifest.name),
|
|
4451
|
+
source: "custom",
|
|
4452
|
+
version: custom.manifest.version,
|
|
4453
|
+
events: custom.manifest.events,
|
|
4454
|
+
description: custom.manifest.description ?? "Custom hook",
|
|
4455
|
+
scriptPath: custom.scriptPath,
|
|
4456
|
+
meta: meta2,
|
|
4457
|
+
timeoutMs: custom.manifest.timeout_ms ?? null
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
const meta = getHook(shortManifestName(name));
|
|
4461
|
+
const scriptPath = resolveScriptPath(name);
|
|
4462
|
+
if (!meta || !scriptPath)
|
|
4463
|
+
return;
|
|
4464
|
+
return {
|
|
4465
|
+
name: shortManifestName(name),
|
|
4466
|
+
source: "bundled",
|
|
4467
|
+
version: meta.version,
|
|
4468
|
+
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
4469
|
+
description: meta.description,
|
|
4470
|
+
scriptPath,
|
|
4471
|
+
meta,
|
|
4472
|
+
timeoutMs: null
|
|
4473
|
+
};
|
|
4607
4474
|
}
|
|
4608
|
-
var
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
});
|
|
4612
|
-
|
|
4613
|
-
// src/lib/semver.ts
|
|
4614
|
-
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4615
|
-
var init_semver = __esm(() => {
|
|
4616
|
-
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4617
|
-
});
|
|
4618
|
-
|
|
4619
|
-
// src/lib/manifest.ts
|
|
4620
|
-
var exports_manifest = {};
|
|
4621
|
-
__export(exports_manifest, {
|
|
4622
|
-
writeCustomHook: () => writeCustomHook,
|
|
4623
|
-
shortManifestName: () => shortManifestName,
|
|
4624
|
-
scriptRelFor: () => scriptRelFor,
|
|
4625
|
-
resolveScript: () => resolveScript,
|
|
4626
|
-
readCustomManifest: () => readCustomManifest,
|
|
4627
|
-
parseManifest: () => parseManifest,
|
|
4628
|
-
normalizeManifestName: () => normalizeManifestName,
|
|
4629
|
-
manifestSchema: () => manifestSchema,
|
|
4630
|
-
listCustomHooks: () => listCustomHooks,
|
|
4631
|
-
isInlineScript: () => isInlineScript,
|
|
4632
|
-
customHookDir: () => customHookDir,
|
|
4633
|
-
assertContained: () => assertContained,
|
|
4634
|
-
ScriptContainmentError: () => ScriptContainmentError,
|
|
4635
|
-
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4636
|
-
});
|
|
4637
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4638
|
-
import { basename, dirname, isAbsolute, join as join9, normalize, resolve as resolve3, sep } from "path";
|
|
4639
|
-
import { z } from "zod";
|
|
4640
|
-
function parseManifest(json) {
|
|
4641
|
-
const parsed = JSON.parse(json);
|
|
4642
|
-
return manifestSchema.parse(parsed);
|
|
4643
|
-
}
|
|
4644
|
-
function normalizeManifestName(name) {
|
|
4645
|
-
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4646
|
-
}
|
|
4647
|
-
function shortManifestName(name) {
|
|
4648
|
-
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4649
|
-
}
|
|
4650
|
-
function isWithin(child, parent) {
|
|
4651
|
-
return child === parent || child.startsWith(parent + sep);
|
|
4652
|
-
}
|
|
4653
|
-
function assertContainedLexical(target, root, raw) {
|
|
4654
|
-
const lexical = resolve3(target);
|
|
4655
|
-
const rootLexical = resolve3(root);
|
|
4656
|
-
if (!isWithin(lexical, rootLexical)) {
|
|
4657
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
4658
|
-
}
|
|
4659
|
-
return lexical;
|
|
4660
|
-
}
|
|
4661
|
-
function realpathNearest(path) {
|
|
4662
|
-
const tail = [];
|
|
4663
|
-
let cur = path;
|
|
4664
|
-
while (!existsSync6(cur)) {
|
|
4665
|
-
const parent = dirname(cur);
|
|
4666
|
-
if (parent === cur)
|
|
4667
|
-
break;
|
|
4668
|
-
tail.unshift(basename(cur));
|
|
4669
|
-
cur = parent;
|
|
4670
|
-
}
|
|
4671
|
-
return join9(realpathSync(cur), ...tail);
|
|
4672
|
-
}
|
|
4673
|
-
function assertContainedReal(root, target, raw) {
|
|
4674
|
-
const rootReal = realpathSync(root);
|
|
4675
|
-
const targetReal = realpathNearest(target);
|
|
4676
|
-
if (!isWithin(targetReal, rootReal)) {
|
|
4677
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
4678
|
-
}
|
|
4679
|
-
}
|
|
4680
|
-
function assertContained(target, root, raw) {
|
|
4681
|
-
const lexical = assertContainedLexical(target, root, raw);
|
|
4682
|
-
assertContainedReal(root, lexical, raw);
|
|
4683
|
-
return lexical;
|
|
4684
|
-
}
|
|
4685
|
-
function isInlineScript(manifest) {
|
|
4686
|
-
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4687
|
-
`);
|
|
4688
|
-
}
|
|
4689
|
-
function scriptRelFor(manifest) {
|
|
4690
|
-
if (isInlineScript(manifest)) {
|
|
4691
|
-
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4692
|
-
return `script${ext}`;
|
|
4693
|
-
}
|
|
4694
|
-
const rel = normalize(manifest.script);
|
|
4695
|
-
if (isAbsolute(rel)) {
|
|
4696
|
-
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4697
|
-
}
|
|
4698
|
-
return rel;
|
|
4699
|
-
}
|
|
4700
|
-
function resolveScript(manifest, manifestDir) {
|
|
4701
|
-
if (isInlineScript(manifest))
|
|
4702
|
-
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4703
|
-
const rel = scriptRelFor(manifest);
|
|
4704
|
-
const resolved = assertContained(join9(manifestDir, rel), manifestDir, manifest.script);
|
|
4705
|
-
if (!existsSync6(resolved)) {
|
|
4706
|
-
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4707
|
-
}
|
|
4708
|
-
return { path: rel, content: readFileSync3(resolved, "utf-8") };
|
|
4709
|
-
}
|
|
4710
|
-
function customHookDir(name) {
|
|
4711
|
-
return join9(getCustomHooksDir(), shortManifestName(name));
|
|
4712
|
-
}
|
|
4713
|
-
function readCustomManifest(name) {
|
|
4714
|
-
const dir = customHookDir(name);
|
|
4715
|
-
const manifestPath = join9(dir, "manifest.json");
|
|
4716
|
-
if (!existsSync6(manifestPath))
|
|
4717
|
-
return;
|
|
4718
|
-
try {
|
|
4719
|
-
const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
|
|
4720
|
-
const script = resolveScript(manifest, dir);
|
|
4721
|
-
const scriptIsInline = isInlineScript(manifest);
|
|
4722
|
-
return { manifest, scriptPath: join9(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4723
|
-
} catch (err) {
|
|
4724
|
-
if (err instanceof ScriptContainmentError)
|
|
4725
|
-
throw err;
|
|
4726
|
-
return;
|
|
4727
|
-
}
|
|
4728
|
-
}
|
|
4729
|
-
function listCustomHooks() {
|
|
4730
|
-
const dir = getCustomHooksDir();
|
|
4731
|
-
if (!existsSync6(dir))
|
|
4732
|
-
return [];
|
|
4733
|
-
const results = [];
|
|
4734
|
-
for (const entry of readdirSafe(dir)) {
|
|
4735
|
-
const parsed = readCustomManifest(entry);
|
|
4736
|
-
if (parsed)
|
|
4737
|
-
results.push(parsed);
|
|
4738
|
-
}
|
|
4739
|
-
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4740
|
-
}
|
|
4741
|
-
function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
4742
|
-
const dir = customHookDir(name);
|
|
4743
|
-
if (isAbsolute(scriptRelPath)) {
|
|
4744
|
-
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4745
|
-
}
|
|
4746
|
-
const scriptPath = scriptRelPath.includes("/") ? join9(dir, scriptRelPath) : join9(dir, scriptRelPath);
|
|
4747
|
-
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4748
|
-
mkdirSync2(dir, { recursive: true });
|
|
4749
|
-
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4750
|
-
const scriptDir = dirname(scriptPath);
|
|
4751
|
-
if (scriptDir !== dir)
|
|
4752
|
-
mkdirSync2(scriptDir, { recursive: true });
|
|
4753
|
-
writeFileSync2(scriptPath, scriptContent, "utf-8");
|
|
4754
|
-
writeFileSync2(join9(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4755
|
-
`, "utf-8");
|
|
4756
|
-
return { dir, scriptPath };
|
|
4757
|
-
}
|
|
4758
|
-
function readdirSafe(dir) {
|
|
4759
|
-
try {
|
|
4760
|
-
return readdirSync(dir);
|
|
4761
|
-
} catch {
|
|
4762
|
-
return [];
|
|
4763
|
-
}
|
|
4764
|
-
}
|
|
4765
|
-
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4766
|
-
var init_manifest = __esm(() => {
|
|
4767
|
-
init_config();
|
|
4768
|
-
init_semver();
|
|
4769
|
-
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4770
|
-
manifestSchema = z.object({
|
|
4771
|
-
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4772
|
-
version: z.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4773
|
-
description: z.string().optional(),
|
|
4774
|
-
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4775
|
-
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4776
|
-
script_kind: z.enum(["inline", "file"]).optional(),
|
|
4777
|
-
args: z.array(z.string()).optional(),
|
|
4778
|
-
timeout_ms: z.number().int().positive().optional(),
|
|
4779
|
-
env: z.record(z.string(), z.string()).optional()
|
|
4780
|
-
});
|
|
4781
|
-
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4782
|
-
constructor(path, hookDir, raw) {
|
|
4783
|
-
super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
|
|
4784
|
-
this.name = "ScriptContainmentError";
|
|
4785
|
-
}
|
|
4786
|
-
};
|
|
4787
|
-
});
|
|
4788
|
-
|
|
4789
|
-
// src/lib/resolve.ts
|
|
4790
|
-
var exports_resolve = {};
|
|
4791
|
-
__export(exports_resolve, {
|
|
4792
|
-
resolveScriptPath: () => resolveScriptPath,
|
|
4793
|
-
resolveHookMeta: () => resolveHookMeta,
|
|
4794
|
-
resolveHookDir: () => resolveHookDir,
|
|
4795
|
-
resolveHook: () => resolveHook,
|
|
4796
|
-
resolveBundledHooksDir: () => resolveBundledHooksDir
|
|
4797
|
-
});
|
|
4798
|
-
import { dirname as dirname2, join as join10 } from "path";
|
|
4799
|
-
import { existsSync as existsSync7 } from "fs";
|
|
4800
|
-
import { fileURLToPath } from "url";
|
|
4801
|
-
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
4802
|
-
const sourceLayout = join10(moduleDir, "..", "..", "hooks");
|
|
4803
|
-
if (existsSync7(join10(sourceLayout, "hook-gitguard")))
|
|
4804
|
-
return sourceLayout;
|
|
4805
|
-
return join10(moduleDir, "..", "hooks");
|
|
4806
|
-
}
|
|
4807
|
-
function resolveHookDir(name) {
|
|
4808
|
-
const custom = readCustomManifest(name);
|
|
4809
|
-
if (custom)
|
|
4810
|
-
return customHookDir(name);
|
|
4811
|
-
const meta = getHook(shortManifestName(name));
|
|
4812
|
-
if (!meta)
|
|
4813
|
-
return;
|
|
4814
|
-
const short = shortManifestName(name);
|
|
4815
|
-
const bundledBase = resolveBundledHooksDir();
|
|
4816
|
-
const direct = join10(bundledBase, short);
|
|
4817
|
-
if (existsSync7(direct))
|
|
4818
|
-
return direct;
|
|
4819
|
-
const prefixed = join10(bundledBase, `hook-${short}`);
|
|
4820
|
-
return existsSync7(prefixed) ? prefixed : undefined;
|
|
4821
|
-
}
|
|
4822
|
-
function resolveScriptPath(name) {
|
|
4823
|
-
const custom = readCustomManifest(name);
|
|
4824
|
-
if (custom)
|
|
4825
|
-
return custom.scriptPath;
|
|
4826
|
-
const dir = resolveHookDir(name);
|
|
4827
|
-
if (!dir)
|
|
4828
|
-
return;
|
|
4829
|
-
const script = join10(dir, "src", "hook.ts");
|
|
4830
|
-
return existsSync7(script) ? script : undefined;
|
|
4831
|
-
}
|
|
4832
|
-
function resolveHookMeta(name) {
|
|
4833
|
-
const custom = readCustomManifest(name);
|
|
4834
|
-
if (custom) {
|
|
4835
|
-
const { manifest } = custom;
|
|
4836
|
-
return {
|
|
4837
|
-
name: shortManifestName(manifest.name),
|
|
4838
|
-
displayName: manifest.description ? manifest.name : manifest.name,
|
|
4839
|
-
description: manifest.description ?? "Custom hook",
|
|
4840
|
-
version: manifest.version,
|
|
4841
|
-
category: "Workflow Automation",
|
|
4842
|
-
event: manifest.events[0] ?? "PostToolUse",
|
|
4843
|
-
events: manifest.events,
|
|
4844
|
-
matcher: "",
|
|
4845
|
-
tags: ["custom"]
|
|
4846
|
-
};
|
|
4847
|
-
}
|
|
4848
|
-
return getHook(shortManifestName(name));
|
|
4849
|
-
}
|
|
4850
|
-
function resolveHook(name) {
|
|
4851
|
-
const custom = readCustomManifest(name);
|
|
4852
|
-
if (custom) {
|
|
4853
|
-
const meta2 = resolveHookMeta(name);
|
|
4854
|
-
return {
|
|
4855
|
-
name: shortManifestName(custom.manifest.name),
|
|
4856
|
-
source: "custom",
|
|
4857
|
-
version: custom.manifest.version,
|
|
4858
|
-
events: custom.manifest.events,
|
|
4859
|
-
description: custom.manifest.description ?? "Custom hook",
|
|
4860
|
-
scriptPath: custom.scriptPath,
|
|
4861
|
-
meta: meta2,
|
|
4862
|
-
timeoutMs: custom.manifest.timeout_ms ?? null
|
|
4863
|
-
};
|
|
4864
|
-
}
|
|
4865
|
-
const meta = getHook(shortManifestName(name));
|
|
4866
|
-
const scriptPath = resolveScriptPath(name);
|
|
4867
|
-
if (!meta || !scriptPath)
|
|
4868
|
-
return;
|
|
4869
|
-
return {
|
|
4870
|
-
name: shortManifestName(name),
|
|
4871
|
-
source: "bundled",
|
|
4872
|
-
version: meta.version,
|
|
4873
|
-
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
4874
|
-
description: meta.description,
|
|
4875
|
-
scriptPath,
|
|
4876
|
-
meta,
|
|
4877
|
-
timeoutMs: null
|
|
4878
|
-
};
|
|
4879
|
-
}
|
|
4880
|
-
var init_resolve = __esm(() => {
|
|
4881
|
-
init_registry();
|
|
4882
|
-
init_manifest();
|
|
4475
|
+
var init_resolve = __esm(() => {
|
|
4476
|
+
init_registry();
|
|
4477
|
+
init_manifest();
|
|
4883
4478
|
});
|
|
4884
4479
|
|
|
4885
4480
|
// src/db/schema.ts
|
|
@@ -5076,9 +4671,9 @@ var init_migrations = __esm(() => {
|
|
|
5076
4671
|
});
|
|
5077
4672
|
|
|
5078
4673
|
// src/db/legacy-import.ts
|
|
5079
|
-
import { existsSync as
|
|
5080
|
-
import { join as
|
|
5081
|
-
import { homedir as
|
|
4674
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
4675
|
+
import { join as join8 } from "path";
|
|
4676
|
+
import { homedir as homedir3 } from "os";
|
|
5082
4677
|
function ensureMetaTable(db) {
|
|
5083
4678
|
db.exec(`
|
|
5084
4679
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -5101,7 +4696,7 @@ function nanoid() {
|
|
|
5101
4696
|
function importJsonlFile(db, filePath) {
|
|
5102
4697
|
let count = 0;
|
|
5103
4698
|
try {
|
|
5104
|
-
const lines =
|
|
4699
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5105
4700
|
`).filter(Boolean);
|
|
5106
4701
|
for (const line of lines) {
|
|
5107
4702
|
try {
|
|
@@ -5127,7 +4722,7 @@ function importJsonlFile(db, filePath) {
|
|
|
5127
4722
|
function importErrorsLog(db, filePath) {
|
|
5128
4723
|
let count = 0;
|
|
5129
4724
|
try {
|
|
5130
|
-
const lines =
|
|
4725
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5131
4726
|
`).filter(Boolean);
|
|
5132
4727
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
5133
4728
|
for (const line of lines) {
|
|
@@ -5152,25 +4747,25 @@ function importErrorsLog(db, filePath) {
|
|
|
5152
4747
|
} catch {}
|
|
5153
4748
|
return count;
|
|
5154
4749
|
}
|
|
5155
|
-
function runLegacyImport(db,
|
|
4750
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
5156
4751
|
try {
|
|
5157
4752
|
if (isAlreadyDone(db))
|
|
5158
4753
|
return;
|
|
5159
4754
|
let total = 0;
|
|
5160
|
-
const claudeProjectsDir =
|
|
5161
|
-
if (
|
|
4755
|
+
const claudeProjectsDir = join8(homeDir, ".claude", "projects");
|
|
4756
|
+
if (existsSync7(claudeProjectsDir)) {
|
|
5162
4757
|
try {
|
|
5163
4758
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
5164
4759
|
for (const dir of projectDirs) {
|
|
5165
|
-
const projectDir =
|
|
4760
|
+
const projectDir = join8(claudeProjectsDir, dir);
|
|
5166
4761
|
try {
|
|
5167
4762
|
const files = readdirSync2(projectDir);
|
|
5168
4763
|
for (const file of files) {
|
|
5169
4764
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
5170
|
-
total += importJsonlFile(db,
|
|
4765
|
+
total += importJsonlFile(db, join8(projectDir, file));
|
|
5171
4766
|
}
|
|
5172
4767
|
if (file === "errors.log") {
|
|
5173
|
-
total += importErrorsLog(db,
|
|
4768
|
+
total += importErrorsLog(db, join8(projectDir, file));
|
|
5174
4769
|
}
|
|
5175
4770
|
}
|
|
5176
4771
|
} catch {}
|
|
@@ -5213,13 +4808,13 @@ __export(exports_db, {
|
|
|
5213
4808
|
closeDb: () => closeDb
|
|
5214
4809
|
});
|
|
5215
4810
|
import { Database } from "bun:sqlite";
|
|
5216
|
-
import { existsSync as
|
|
5217
|
-
import { join as
|
|
4811
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
4812
|
+
import { join as join9 } from "path";
|
|
5218
4813
|
function resolveDataDir() {
|
|
5219
4814
|
const effective = getEffectiveDataRoot();
|
|
5220
|
-
const oldDir =
|
|
5221
|
-
if (!
|
|
5222
|
-
|
|
4815
|
+
const oldDir = join9(getHomeDir(), ".hooks");
|
|
4816
|
+
if (!existsSync8(effective) && existsSync8(oldDir)) {
|
|
4817
|
+
mkdirSync2(effective, { recursive: true });
|
|
5223
4818
|
cpSync(oldDir, effective, { recursive: true });
|
|
5224
4819
|
}
|
|
5225
4820
|
return effective;
|
|
@@ -5229,19 +4824,19 @@ function getDbPath() {
|
|
|
5229
4824
|
if (explicitDb)
|
|
5230
4825
|
return explicitDb;
|
|
5231
4826
|
const dataDir3 = resolveDataDir();
|
|
5232
|
-
return
|
|
4827
|
+
return join9(dataDir3, "hooks.db");
|
|
5233
4828
|
}
|
|
5234
4829
|
function ensureDir(dbPath) {
|
|
5235
4830
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5236
|
-
if (dir && !
|
|
5237
|
-
|
|
4831
|
+
if (dir && !existsSync8(dir)) {
|
|
4832
|
+
mkdirSync2(dir, { recursive: true });
|
|
5238
4833
|
}
|
|
5239
4834
|
}
|
|
5240
4835
|
function getDb() {
|
|
5241
4836
|
if (instance)
|
|
5242
4837
|
return instance;
|
|
5243
4838
|
const dbPath = getDbPath();
|
|
5244
|
-
const isNew = dbPath === ":memory:" || !
|
|
4839
|
+
const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
|
|
5245
4840
|
ensureDir(dbPath);
|
|
5246
4841
|
instance = new Database(dbPath);
|
|
5247
4842
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -5304,7 +4899,7 @@ __export(exports_store, {
|
|
|
5304
4899
|
LockFileError: () => LockFileError
|
|
5305
4900
|
});
|
|
5306
4901
|
import { createHash } from "crypto";
|
|
5307
|
-
import { existsSync as
|
|
4902
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5308
4903
|
import { readFile as readFile2 } from "fs/promises";
|
|
5309
4904
|
import { randomBytes } from "crypto";
|
|
5310
4905
|
function sha256Of(input) {
|
|
@@ -5366,11 +4961,11 @@ function assertLockShape(parsed, path) {
|
|
|
5366
4961
|
}
|
|
5367
4962
|
function readLock() {
|
|
5368
4963
|
const path = getLockPath();
|
|
5369
|
-
if (!
|
|
4964
|
+
if (!existsSync9(path))
|
|
5370
4965
|
return { hooks: {} };
|
|
5371
4966
|
let raw;
|
|
5372
4967
|
try {
|
|
5373
|
-
raw =
|
|
4968
|
+
raw = readFileSync3(path, "utf-8");
|
|
5374
4969
|
} catch {
|
|
5375
4970
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5376
4971
|
}
|
|
@@ -5385,13 +4980,13 @@ function readLock() {
|
|
|
5385
4980
|
}
|
|
5386
4981
|
function writeLock(lock) {
|
|
5387
4982
|
const path = getLockPath();
|
|
5388
|
-
|
|
4983
|
+
mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5389
4984
|
const sorted = {};
|
|
5390
4985
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5391
4986
|
sorted[name] = lock.hooks[name];
|
|
5392
4987
|
}
|
|
5393
4988
|
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
5394
|
-
|
|
4989
|
+
writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5395
4990
|
`, "utf-8");
|
|
5396
4991
|
renameSync(tmp, path);
|
|
5397
4992
|
return path;
|
|
@@ -5467,7 +5062,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5467
5062
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5468
5063
|
}
|
|
5469
5064
|
function retrustHook(name, scriptPath, version, source) {
|
|
5470
|
-
const actual = sha256Of(
|
|
5065
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5471
5066
|
const db = getDb();
|
|
5472
5067
|
upsertHookRecord(db, {
|
|
5473
5068
|
name,
|
|
@@ -5508,9 +5103,9 @@ __export(exports_installer, {
|
|
|
5508
5103
|
getHookPath: () => getHookPath,
|
|
5509
5104
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5510
5105
|
});
|
|
5511
|
-
import { existsSync as
|
|
5512
|
-
import { join as
|
|
5513
|
-
import { homedir as
|
|
5106
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5107
|
+
import { join as join10, dirname as dirname3 } from "path";
|
|
5108
|
+
import { homedir as homedir4 } from "os";
|
|
5514
5109
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5515
5110
|
function normalizeHookName(name) {
|
|
5516
5111
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -5549,25 +5144,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5549
5144
|
return globalOverride;
|
|
5550
5145
|
const dir = getTargetSettingsDir(target);
|
|
5551
5146
|
if (scope === "project") {
|
|
5552
|
-
return target === "codewith" ?
|
|
5147
|
+
return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
|
|
5553
5148
|
}
|
|
5554
|
-
return target === "codewith" ?
|
|
5149
|
+
return target === "codewith" ? join10(homedir4(), dir, "config.toml") : join10(homedir4(), dir, "settings.json");
|
|
5555
5150
|
}
|
|
5556
5151
|
function getHookPath(name) {
|
|
5557
5152
|
const shortName = shortHookName(name);
|
|
5558
5153
|
const resolved = resolveHookDir(shortName);
|
|
5559
5154
|
if (resolved)
|
|
5560
5155
|
return resolved;
|
|
5561
|
-
return
|
|
5156
|
+
return join10(HOOKS_DIR, normalizeHookName(shortName));
|
|
5562
5157
|
}
|
|
5563
5158
|
function hookExists(name) {
|
|
5564
|
-
return
|
|
5159
|
+
return existsSync10(getHookPath(name));
|
|
5565
5160
|
}
|
|
5566
5161
|
function readSettings(scope = "global", target = "claude") {
|
|
5567
5162
|
const path = getSettingsPath(scope, target);
|
|
5568
5163
|
try {
|
|
5569
|
-
if (
|
|
5570
|
-
return JSON.parse(
|
|
5164
|
+
if (existsSync10(path)) {
|
|
5165
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
5571
5166
|
}
|
|
5572
5167
|
} catch (error) {
|
|
5573
5168
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5577,10 +5172,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
5577
5172
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
5578
5173
|
const path = getSettingsPath(scope, target);
|
|
5579
5174
|
const dir = dirname3(path);
|
|
5580
|
-
if (!
|
|
5581
|
-
|
|
5175
|
+
if (!existsSync10(dir)) {
|
|
5176
|
+
mkdirSync4(dir, { recursive: true });
|
|
5582
5177
|
}
|
|
5583
|
-
|
|
5178
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
5584
5179
|
`);
|
|
5585
5180
|
}
|
|
5586
5181
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -5666,7 +5261,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
5666
5261
|
function readCodewithConfig(scope, configPath) {
|
|
5667
5262
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5668
5263
|
try {
|
|
5669
|
-
return
|
|
5264
|
+
return existsSync10(path) ? readFileSync4(path, "utf-8") : "";
|
|
5670
5265
|
} catch {
|
|
5671
5266
|
return "";
|
|
5672
5267
|
}
|
|
@@ -5674,13 +5269,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
5674
5269
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5675
5270
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5676
5271
|
const dir = dirname3(path);
|
|
5677
|
-
if (!
|
|
5678
|
-
|
|
5679
|
-
const existing =
|
|
5272
|
+
if (!existsSync10(dir))
|
|
5273
|
+
mkdirSync4(dir, { recursive: true });
|
|
5274
|
+
const existing = existsSync10(path) ? readFileSync4(path, "utf-8") : "";
|
|
5680
5275
|
const sep2 = existing.trim() ? `
|
|
5681
5276
|
|
|
5682
5277
|
` : "";
|
|
5683
|
-
|
|
5278
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
5684
5279
|
return path;
|
|
5685
5280
|
}
|
|
5686
5281
|
function detectConflict(name, scope, target) {
|
|
@@ -5855,460 +5450,1347 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
5855
5450
|
}
|
|
5856
5451
|
}
|
|
5857
5452
|
}
|
|
5858
|
-
function unregisterHook(name, scope = "global", target = "claude") {
|
|
5859
|
-
const settings = readSettings(scope, target);
|
|
5860
|
-
if (!settings.hooks)
|
|
5453
|
+
function unregisterHook(name, scope = "global", target = "claude") {
|
|
5454
|
+
const settings = readSettings(scope, target);
|
|
5455
|
+
if (!settings.hooks)
|
|
5456
|
+
return;
|
|
5457
|
+
removeHookFromAllEvents(settings, name);
|
|
5458
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
5459
|
+
delete settings.hooks;
|
|
5460
|
+
}
|
|
5461
|
+
writeSettings(settings, scope, target);
|
|
5462
|
+
}
|
|
5463
|
+
function installHooks(names, options = {}) {
|
|
5464
|
+
return names.map((name) => installHook(name, options));
|
|
5465
|
+
}
|
|
5466
|
+
function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
5467
|
+
if (target === "codewith") {
|
|
5468
|
+
const config = readCodewithConfig(scope);
|
|
5469
|
+
const registered2 = [];
|
|
5470
|
+
const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
|
|
5471
|
+
let match;
|
|
5472
|
+
while (match = re.exec(config)) {
|
|
5473
|
+
registered2.push(match[1]);
|
|
5474
|
+
}
|
|
5475
|
+
return [...new Set(registered2)];
|
|
5476
|
+
}
|
|
5477
|
+
const settings = readSettings(scope, target);
|
|
5478
|
+
if (!settings.hooks)
|
|
5479
|
+
return [];
|
|
5480
|
+
const registered = [];
|
|
5481
|
+
for (const eventKey of Object.keys(settings.hooks)) {
|
|
5482
|
+
for (const entry of settings.hooks[eventKey]) {
|
|
5483
|
+
for (const hook of entry.hooks || []) {
|
|
5484
|
+
const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
|
|
5485
|
+
const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
|
|
5486
|
+
const match = newMatch || oldMatch;
|
|
5487
|
+
if (match) {
|
|
5488
|
+
registered.push(match[1]);
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
return [...new Set(registered)];
|
|
5494
|
+
}
|
|
5495
|
+
function getRegisteredHooks(scope = "global") {
|
|
5496
|
+
return getRegisteredHooksForTarget(scope, "claude");
|
|
5497
|
+
}
|
|
5498
|
+
function removeHook(name, scope = "global", target = "claude") {
|
|
5499
|
+
const shortName = shortHookName(name);
|
|
5500
|
+
if (target === "all") {
|
|
5501
|
+
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
5502
|
+
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
5503
|
+
return claudeRemoved || geminiRemoved;
|
|
5504
|
+
}
|
|
5505
|
+
if (target === "codewith") {
|
|
5506
|
+
return false;
|
|
5507
|
+
}
|
|
5508
|
+
return removeHookForTarget(shortName, scope, target);
|
|
5509
|
+
}
|
|
5510
|
+
function removeHookForTarget(name, scope, target) {
|
|
5511
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5512
|
+
if (!registered.includes(name)) {
|
|
5513
|
+
return false;
|
|
5514
|
+
}
|
|
5515
|
+
unregisterHook(name, scope, target);
|
|
5516
|
+
return true;
|
|
5517
|
+
}
|
|
5518
|
+
function removeCodewithHookEntry(configText, name) {
|
|
5519
|
+
const sections = configText.split(/\n\s*\n/);
|
|
5520
|
+
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
5521
|
+
const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
|
|
5522
|
+
const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
|
|
5523
|
+
const eventsWithEntries = new Set;
|
|
5524
|
+
const kept = [];
|
|
5525
|
+
let removed = false;
|
|
5526
|
+
for (const section of sections) {
|
|
5527
|
+
const lines = section.split(`
|
|
5528
|
+
`);
|
|
5529
|
+
const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
|
|
5530
|
+
if (entryMatch) {
|
|
5531
|
+
const event = entryMatch[1];
|
|
5532
|
+
eventsWithEntries.add(event);
|
|
5533
|
+
if (lines.some((line) => nameRe.test(line.trim()))) {
|
|
5534
|
+
removed = true;
|
|
5535
|
+
continue;
|
|
5536
|
+
}
|
|
5537
|
+
kept.push({ text: section });
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
|
|
5541
|
+
kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
|
|
5542
|
+
}
|
|
5543
|
+
const keptEntriesByEvent = new Set;
|
|
5544
|
+
for (const item of kept) {
|
|
5545
|
+
const m = entryHeaderRe.exec(item.text.split(`
|
|
5546
|
+
`)[0] ?? "");
|
|
5547
|
+
if (m)
|
|
5548
|
+
keptEntriesByEvent.add(m[1]);
|
|
5549
|
+
}
|
|
5550
|
+
const finalSections = kept.filter((item) => {
|
|
5551
|
+
if (item.eventHeaderFor === undefined)
|
|
5552
|
+
return true;
|
|
5553
|
+
if (!eventsWithEntries.has(item.eventHeaderFor))
|
|
5554
|
+
return true;
|
|
5555
|
+
return keptEntriesByEvent.has(item.eventHeaderFor);
|
|
5556
|
+
}).map((item) => item.text);
|
|
5557
|
+
return {
|
|
5558
|
+
text: finalSections.join(`
|
|
5559
|
+
|
|
5560
|
+
`) + (configText.endsWith(`
|
|
5561
|
+
`) ? `
|
|
5562
|
+
` : ""),
|
|
5563
|
+
removed
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
function escapeRegExp(value) {
|
|
5567
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5568
|
+
}
|
|
5569
|
+
function codewithHasHookEntryInText(configText, name) {
|
|
5570
|
+
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
5571
|
+
}
|
|
5572
|
+
function codewithHasHookEntry(name, scope = "global") {
|
|
5573
|
+
const path = getSettingsPath(scope, "codewith");
|
|
5574
|
+
if (!existsSync10(path))
|
|
5575
|
+
return false;
|
|
5576
|
+
try {
|
|
5577
|
+
return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
|
|
5578
|
+
} catch {
|
|
5579
|
+
return false;
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
function removeStoreDirFailClosed(dir) {
|
|
5583
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
5584
|
+
for (const entry of entries) {
|
|
5585
|
+
if (entry.isDirectory()) {
|
|
5586
|
+
removeStoreDirFailClosed(join10(dir, entry.name));
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
for (const entry of entries) {
|
|
5590
|
+
if (!entry.isDirectory()) {
|
|
5591
|
+
unlinkSync(join10(dir, entry.name));
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
rmdirSync(dir);
|
|
5595
|
+
}
|
|
5596
|
+
function uninstallHook(name, scope = "global", target = "claude") {
|
|
5597
|
+
const shortName = shortHookName(name);
|
|
5598
|
+
const custom = readCustomManifest(shortName);
|
|
5599
|
+
const bundledMeta = custom ? undefined : getHook(shortName);
|
|
5600
|
+
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
5601
|
+
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
5602
|
+
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5603
|
+
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5604
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
5605
|
+
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5606
|
+
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5607
|
+
}
|
|
5608
|
+
const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
|
|
5609
|
+
const scopes = [];
|
|
5610
|
+
for (const s of ["global", "project"]) {
|
|
5611
|
+
for (const t of writableTargets) {
|
|
5612
|
+
if (getRegisteredHooksForTarget(s, t).includes(shortName)) {
|
|
5613
|
+
unregisterHook(shortName, s, t);
|
|
5614
|
+
if (!scopes.includes(s))
|
|
5615
|
+
scopes.push(s);
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
const registrationsRemaining = [];
|
|
5620
|
+
if (target === "codewith" || target === "all") {
|
|
5621
|
+
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5622
|
+
if (existsSync10(codewithPath)) {
|
|
5623
|
+
const before = readFileSync4(codewithPath, "utf-8");
|
|
5624
|
+
const after = removeCodewithHookEntry(before, shortName);
|
|
5625
|
+
if (after.removed) {
|
|
5626
|
+
writeFileSync3(codewithPath, after.text, "utf-8");
|
|
5627
|
+
}
|
|
5628
|
+
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
5629
|
+
registrationsRemaining.push("codewith");
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
if (custom) {
|
|
5634
|
+
try {
|
|
5635
|
+
removeStoreDirFailClosed(customHookDir(shortName));
|
|
5636
|
+
} catch {
|
|
5637
|
+
return {
|
|
5638
|
+
name: shortName,
|
|
5639
|
+
removed: false,
|
|
5640
|
+
source: "custom",
|
|
5641
|
+
settingsScopes: scopes,
|
|
5642
|
+
storeDirRemoved: false,
|
|
5643
|
+
pinRemoved: false,
|
|
5644
|
+
dbRecordRemoved: false,
|
|
5645
|
+
registrationsRemaining,
|
|
5646
|
+
error: `Hook '${shortName}' store directory could not be removed; trust records preserved (no fail-open)`
|
|
5647
|
+
};
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
const { removedPin, removedRecord } = removeHookFromStore(shortName);
|
|
5651
|
+
return {
|
|
5652
|
+
name: shortName,
|
|
5653
|
+
removed: true,
|
|
5654
|
+
source: custom ? "custom" : bundledMeta ? "bundled" : "registered-only",
|
|
5655
|
+
settingsScopes: scopes,
|
|
5656
|
+
storeDirRemoved: custom ? true : false,
|
|
5657
|
+
pinRemoved: removedPin,
|
|
5658
|
+
dbRecordRemoved: removedRecord,
|
|
5659
|
+
registrationsRemaining
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
5663
|
+
var init_installer = __esm(() => {
|
|
5664
|
+
init_registry();
|
|
5665
|
+
init_resolve();
|
|
5666
|
+
init_manifest();
|
|
5667
|
+
init_store();
|
|
5668
|
+
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5669
|
+
HOOKS_DIR = existsSync10(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
|
|
5670
|
+
EVENT_MAP = {
|
|
5671
|
+
claude: {
|
|
5672
|
+
PreToolUse: "PreToolUse",
|
|
5673
|
+
PostToolUse: "PostToolUse",
|
|
5674
|
+
Stop: "Stop",
|
|
5675
|
+
Notification: "Notification",
|
|
5676
|
+
SessionStart: "SessionStart",
|
|
5677
|
+
SessionEnd: "SessionEnd",
|
|
5678
|
+
UserPromptSubmit: null,
|
|
5679
|
+
SubagentStart: null
|
|
5680
|
+
},
|
|
5681
|
+
gemini: {
|
|
5682
|
+
PreToolUse: "BeforeTool",
|
|
5683
|
+
PostToolUse: "AfterTool",
|
|
5684
|
+
Stop: "AfterAgent",
|
|
5685
|
+
Notification: "Notification",
|
|
5686
|
+
SessionStart: null,
|
|
5687
|
+
SessionEnd: null,
|
|
5688
|
+
UserPromptSubmit: null,
|
|
5689
|
+
SubagentStart: null
|
|
5690
|
+
},
|
|
5691
|
+
codewith: {
|
|
5692
|
+
PreToolUse: "PreToolUse",
|
|
5693
|
+
PostToolUse: "PostToolUse",
|
|
5694
|
+
Stop: "Stop",
|
|
5695
|
+
Notification: null,
|
|
5696
|
+
SessionStart: "SessionStart",
|
|
5697
|
+
SessionEnd: null,
|
|
5698
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
5699
|
+
SubagentStart: "SubagentStart"
|
|
5700
|
+
}
|
|
5701
|
+
};
|
|
5702
|
+
getInstalledHooks = getRegisteredHooks;
|
|
5703
|
+
});
|
|
5704
|
+
|
|
5705
|
+
// src/lib/registration.ts
|
|
5706
|
+
function safeMatcherTest(pattern, value) {
|
|
5707
|
+
try {
|
|
5708
|
+
return new RegExp(pattern).test(value);
|
|
5709
|
+
} catch {
|
|
5710
|
+
return false;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
function countSettingsWiring(settings) {
|
|
5714
|
+
const hooks = settings.hooks;
|
|
5715
|
+
if (!hooks || typeof hooks !== "object")
|
|
5716
|
+
return 0;
|
|
5717
|
+
let count = 0;
|
|
5718
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
5719
|
+
const entries = hooks[eventKey];
|
|
5720
|
+
if (!Array.isArray(entries))
|
|
5721
|
+
continue;
|
|
5722
|
+
for (const entry of entries) {
|
|
5723
|
+
if (Array.isArray(entry?.hooks))
|
|
5724
|
+
count += entry.hooks.length;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
return count;
|
|
5728
|
+
}
|
|
5729
|
+
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
5730
|
+
const separator = eventSpec.indexOf(":");
|
|
5731
|
+
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
5732
|
+
const specMatcher = separator === -1 ? "" : eventSpec.slice(separator + 1);
|
|
5733
|
+
const matcher = specMatcher || hookMatcher || "";
|
|
5734
|
+
const hooks = settings.hooks?.[eventName];
|
|
5735
|
+
if (!Array.isArray(hooks))
|
|
5736
|
+
return false;
|
|
5737
|
+
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
5738
|
+
const match = h?.command?.match(/^hooks run ([\w-]+)/);
|
|
5739
|
+
if (!match || match[1] !== name)
|
|
5740
|
+
return false;
|
|
5741
|
+
if (!matcher)
|
|
5742
|
+
return true;
|
|
5743
|
+
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
5744
|
+
if (!entryMatcher)
|
|
5745
|
+
return true;
|
|
5746
|
+
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
5747
|
+
}));
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
// src/lib/redact.ts
|
|
5751
|
+
function redactText(value) {
|
|
5752
|
+
let out = value;
|
|
5753
|
+
for (const pattern of SECRET_SHAPES) {
|
|
5754
|
+
out = out.replace(pattern, REDACTED);
|
|
5755
|
+
}
|
|
5756
|
+
return out;
|
|
5757
|
+
}
|
|
5758
|
+
function redactValue2(value) {
|
|
5759
|
+
if (typeof value === "string")
|
|
5760
|
+
return redactText(value);
|
|
5761
|
+
if (Array.isArray(value))
|
|
5762
|
+
return value.map(redactValue2);
|
|
5763
|
+
if (value !== null && typeof value === "object") {
|
|
5764
|
+
const out = {};
|
|
5765
|
+
for (const [key, child] of Object.entries(value)) {
|
|
5766
|
+
out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
|
|
5767
|
+
}
|
|
5768
|
+
return out;
|
|
5769
|
+
}
|
|
5770
|
+
return value;
|
|
5771
|
+
}
|
|
5772
|
+
function redactEventPayload(text) {
|
|
5773
|
+
if (text === null || text === undefined)
|
|
5774
|
+
return null;
|
|
5775
|
+
try {
|
|
5776
|
+
const parsed = JSON.parse(text);
|
|
5777
|
+
return redactText(JSON.stringify(redactValue2(parsed)));
|
|
5778
|
+
} catch {
|
|
5779
|
+
return redactText(text);
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
function projectEventRowForRead(row) {
|
|
5783
|
+
const out = { ...row };
|
|
5784
|
+
for (const field of ["tool_input", "error", "metadata"]) {
|
|
5785
|
+
const value = out[field];
|
|
5786
|
+
if (typeof value === "string")
|
|
5787
|
+
out[field] = redactEventPayload(value);
|
|
5788
|
+
else if (value === null)
|
|
5789
|
+
out[field] = null;
|
|
5790
|
+
}
|
|
5791
|
+
return out;
|
|
5792
|
+
}
|
|
5793
|
+
var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
|
|
5794
|
+
var init_redact = __esm(() => {
|
|
5795
|
+
SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
|
|
5796
|
+
SECRET_SHAPES = [
|
|
5797
|
+
/\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
|
|
5798
|
+
/\bsk-[A-Za-z0-9]{16,}/g,
|
|
5799
|
+
/\btpe_[A-Za-z0-9]{8,}/g,
|
|
5800
|
+
/\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
5801
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
5802
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
5803
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
5804
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
5805
|
+
/ASIA[0-9A-Z]{16}/g,
|
|
5806
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
5807
|
+
/-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
|
|
5808
|
+
/-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
|
|
5809
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
5810
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
5811
|
+
/\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
|
|
5812
|
+
];
|
|
5813
|
+
});
|
|
5814
|
+
|
|
5815
|
+
// src/lib/profiles.ts
|
|
5816
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
5817
|
+
import { join as join11 } from "path";
|
|
5818
|
+
function getProfilesDir() {
|
|
5819
|
+
const newDir = join11(getEffectiveDataRoot(), "profiles");
|
|
5820
|
+
const oldDir = join11(getHomeDir(), ".hooks", "profiles");
|
|
5821
|
+
if (!existsSync11(newDir) && existsSync11(oldDir)) {
|
|
5822
|
+
mkdirSync5(newDir, { recursive: true });
|
|
5823
|
+
cpSync2(oldDir, newDir, { recursive: true });
|
|
5824
|
+
}
|
|
5825
|
+
return newDir;
|
|
5826
|
+
}
|
|
5827
|
+
function ensureProfilesDir() {
|
|
5828
|
+
const dir = getProfilesDir();
|
|
5829
|
+
if (!existsSync11(dir)) {
|
|
5830
|
+
mkdirSync5(dir, { recursive: true });
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
function profilePath(id) {
|
|
5834
|
+
return join11(getProfilesDir(), `${id}.json`);
|
|
5835
|
+
}
|
|
5836
|
+
function shortUuid() {
|
|
5837
|
+
return crypto.randomUUID().slice(0, 8);
|
|
5838
|
+
}
|
|
5839
|
+
function createProfile(input) {
|
|
5840
|
+
ensureProfilesDir();
|
|
5841
|
+
const id = shortUuid();
|
|
5842
|
+
const now2 = new Date().toISOString();
|
|
5843
|
+
const profile = {
|
|
5844
|
+
agent_id: id,
|
|
5845
|
+
agent_type: input.agent_type,
|
|
5846
|
+
created_at: now2,
|
|
5847
|
+
last_seen_at: now2,
|
|
5848
|
+
preferences: {}
|
|
5849
|
+
};
|
|
5850
|
+
if (input.name) {
|
|
5851
|
+
profile.name = input.name;
|
|
5852
|
+
}
|
|
5853
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5854
|
+
`);
|
|
5855
|
+
return profile;
|
|
5856
|
+
}
|
|
5857
|
+
function getProfile(id) {
|
|
5858
|
+
const path = profilePath(id);
|
|
5859
|
+
try {
|
|
5860
|
+
if (!existsSync11(path))
|
|
5861
|
+
return null;
|
|
5862
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5863
|
+
} catch {
|
|
5864
|
+
return null;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
function listProfiles() {
|
|
5868
|
+
const dir = getProfilesDir();
|
|
5869
|
+
if (!existsSync11(dir))
|
|
5870
|
+
return [];
|
|
5871
|
+
try {
|
|
5872
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
5873
|
+
const profiles = [];
|
|
5874
|
+
for (const file of files) {
|
|
5875
|
+
try {
|
|
5876
|
+
const content = readFileSync5(join11(dir, file), "utf-8");
|
|
5877
|
+
profiles.push(JSON.parse(content));
|
|
5878
|
+
} catch {}
|
|
5879
|
+
}
|
|
5880
|
+
return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
5881
|
+
} catch {
|
|
5882
|
+
return [];
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
function touchProfile(id) {
|
|
5886
|
+
const profile = getProfile(id);
|
|
5887
|
+
if (!profile)
|
|
5861
5888
|
return;
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
}
|
|
5866
|
-
writeSettings(settings, scope, target);
|
|
5889
|
+
profile.last_seen_at = new Date().toISOString();
|
|
5890
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5891
|
+
`);
|
|
5867
5892
|
}
|
|
5868
|
-
function
|
|
5869
|
-
return
|
|
5893
|
+
function exportProfiles() {
|
|
5894
|
+
return listProfiles();
|
|
5870
5895
|
}
|
|
5871
|
-
function
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5896
|
+
function importProfiles(profiles) {
|
|
5897
|
+
ensureProfilesDir();
|
|
5898
|
+
let imported = 0;
|
|
5899
|
+
let skipped = 0;
|
|
5900
|
+
for (const profile of profiles) {
|
|
5901
|
+
if (!profile.agent_id || !profile.agent_type) {
|
|
5902
|
+
skipped++;
|
|
5903
|
+
continue;
|
|
5879
5904
|
}
|
|
5880
|
-
|
|
5905
|
+
const path = profilePath(profile.agent_id);
|
|
5906
|
+
if (existsSync11(path)) {
|
|
5907
|
+
skipped++;
|
|
5908
|
+
continue;
|
|
5909
|
+
}
|
|
5910
|
+
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
5911
|
+
`);
|
|
5912
|
+
imported++;
|
|
5881
5913
|
}
|
|
5882
|
-
|
|
5883
|
-
|
|
5914
|
+
return { imported, skipped };
|
|
5915
|
+
}
|
|
5916
|
+
var init_profiles = __esm(() => {
|
|
5917
|
+
init_app_home();
|
|
5918
|
+
});
|
|
5919
|
+
|
|
5920
|
+
// ../contracts/dist/client/transport.js
|
|
5921
|
+
import { isIP as isIP2 } from "net";
|
|
5922
|
+
import { spawnSync } from "child_process";
|
|
5923
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
5924
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5925
|
+
import { createRequire } from "module";
|
|
5926
|
+
import { hostname as osHostname } from "os";
|
|
5927
|
+
import { isAbsolute as isAbsolute2, join as join12 } from "path";
|
|
5928
|
+
function envToken(name) {
|
|
5929
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
5930
|
+
}
|
|
5931
|
+
function clientTransportEnvKeys(name) {
|
|
5932
|
+
const envSegment = envToken(name);
|
|
5933
|
+
return {
|
|
5934
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
5935
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
function credentialOverrideEnvKey(name) {
|
|
5939
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
5940
|
+
}
|
|
5941
|
+
function credentialPointerEnvKey(name) {
|
|
5942
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
5943
|
+
}
|
|
5944
|
+
function homeDir(env) {
|
|
5945
|
+
const home = env.HOME?.trim();
|
|
5946
|
+
return home ? home : null;
|
|
5947
|
+
}
|
|
5948
|
+
function absoluteOverride(env, key) {
|
|
5949
|
+
const value = env[key]?.trim();
|
|
5950
|
+
return value && isAbsolute2(value) ? value : null;
|
|
5951
|
+
}
|
|
5952
|
+
function hasnaHomeDir(env) {
|
|
5953
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
5954
|
+
if (override)
|
|
5955
|
+
return override;
|
|
5956
|
+
const home = homeDir(env);
|
|
5957
|
+
return home ? join12(home, HASNA_HOME_DIR) : null;
|
|
5958
|
+
}
|
|
5959
|
+
function appConfigDir(name, env) {
|
|
5960
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
5961
|
+
if (configRoot)
|
|
5962
|
+
return join12(configRoot, name);
|
|
5963
|
+
const root = hasnaHomeDir(env);
|
|
5964
|
+
return root ? join12(root, name, CONFIG_SUBDIR) : null;
|
|
5965
|
+
}
|
|
5966
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
5967
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
5884
5968
|
return [];
|
|
5885
|
-
const
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5969
|
+
const directory = appConfigDir(name, env);
|
|
5970
|
+
if (!directory)
|
|
5971
|
+
return [];
|
|
5972
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
5973
|
+
return [{ path: join12(directory, file), tier: "disk" }];
|
|
5974
|
+
}
|
|
5975
|
+
function credentialDiskSources(name, env) {
|
|
5976
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
5977
|
+
}
|
|
5978
|
+
function profileDiskSources(name, env, profile) {
|
|
5979
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
5980
|
+
}
|
|
5981
|
+
function parseEnvFile(text) {
|
|
5982
|
+
const values = new Map;
|
|
5983
|
+
const unusable = new Set;
|
|
5984
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5985
|
+
const line = rawLine.trim();
|
|
5986
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
5987
|
+
continue;
|
|
5988
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
5989
|
+
const equals = withoutExport.indexOf("=");
|
|
5990
|
+
if (equals <= 0)
|
|
5991
|
+
continue;
|
|
5992
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
5993
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5994
|
+
continue;
|
|
5995
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
5996
|
+
const quote = value[0];
|
|
5997
|
+
if (quote === '"' || quote === "'") {
|
|
5998
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
5999
|
+
unusable.add(key);
|
|
6000
|
+
continue;
|
|
5895
6001
|
}
|
|
6002
|
+
value = value.slice(1, -1);
|
|
5896
6003
|
}
|
|
6004
|
+
if (value.trim().length === 0) {
|
|
6005
|
+
unusable.add(key);
|
|
6006
|
+
continue;
|
|
6007
|
+
}
|
|
6008
|
+
if (values.has(key) && values.get(key) !== value)
|
|
6009
|
+
unusable.add(key);
|
|
6010
|
+
values.set(key, value);
|
|
5897
6011
|
}
|
|
5898
|
-
return
|
|
6012
|
+
return { values, unusable };
|
|
5899
6013
|
}
|
|
5900
|
-
function
|
|
5901
|
-
|
|
6014
|
+
function configFileModeAllowed(mode) {
|
|
6015
|
+
const permissions = mode & 4095;
|
|
6016
|
+
return permissions === 256 || permissions === 384;
|
|
5902
6017
|
}
|
|
5903
|
-
function
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
6018
|
+
function configFileReadsCoherent(before, after) {
|
|
6019
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
6020
|
+
}
|
|
6021
|
+
function readAppConfigFile(path) {
|
|
6022
|
+
const unsafe = (reason) => {
|
|
6023
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
6024
|
+
};
|
|
6025
|
+
let fd = -1;
|
|
6026
|
+
try {
|
|
6027
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6028
|
+
} catch (error) {
|
|
6029
|
+
const code = error.code;
|
|
6030
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
6031
|
+
return null;
|
|
6032
|
+
if (code === "ELOOP")
|
|
6033
|
+
unsafe("the path is a symlink");
|
|
6034
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
5909
6035
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
6036
|
+
try {
|
|
6037
|
+
const before = fstatSync(fd);
|
|
6038
|
+
if (!before.isFile())
|
|
6039
|
+
unsafe("the path is not a regular file");
|
|
6040
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
6041
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
6042
|
+
}
|
|
6043
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
6044
|
+
if (uid !== undefined && before.uid !== uid)
|
|
6045
|
+
unsafe("the file is not owned by the current user");
|
|
6046
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
6047
|
+
unsafe("the file exceeds the size limit");
|
|
6048
|
+
const bytes = readFileSync6(fd);
|
|
6049
|
+
const after = fstatSync(fd);
|
|
6050
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
6051
|
+
unsafe("the file changed while being read");
|
|
6052
|
+
}
|
|
6053
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
6054
|
+
} finally {
|
|
6055
|
+
if (fd !== -1)
|
|
6056
|
+
closeSync(fd);
|
|
5912
6057
|
}
|
|
5913
|
-
return removeHookForTarget(shortName, scope, target);
|
|
5914
6058
|
}
|
|
5915
|
-
function
|
|
5916
|
-
const
|
|
5917
|
-
if (!
|
|
5918
|
-
return
|
|
6059
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
6060
|
+
const parsed = readAppConfigFile(path);
|
|
6061
|
+
if (!parsed)
|
|
6062
|
+
return null;
|
|
6063
|
+
for (const key of apiKeyKeys) {
|
|
6064
|
+
if (parsed.unusable.has(key)) {
|
|
6065
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
6066
|
+
}
|
|
5919
6067
|
}
|
|
5920
|
-
|
|
5921
|
-
|
|
6068
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
6069
|
+
if (new Set(values).size > 1) {
|
|
6070
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
6071
|
+
}
|
|
6072
|
+
return values[0] ?? null;
|
|
5922
6073
|
}
|
|
5923
|
-
function
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
const
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
let removed = false;
|
|
5931
|
-
for (const section of sections) {
|
|
5932
|
-
const lines = section.split(`
|
|
5933
|
-
`);
|
|
5934
|
-
const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
|
|
5935
|
-
if (entryMatch) {
|
|
5936
|
-
const event = entryMatch[1];
|
|
5937
|
-
eventsWithEntries.add(event);
|
|
5938
|
-
if (lines.some((line) => nameRe.test(line.trim()))) {
|
|
5939
|
-
removed = true;
|
|
5940
|
-
continue;
|
|
5941
|
-
}
|
|
5942
|
-
kept.push({ text: section });
|
|
6074
|
+
function appConfigDiskValue(name, env, keys) {
|
|
6075
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
6076
|
+
if (wanted.length === 0)
|
|
6077
|
+
return null;
|
|
6078
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
6079
|
+
const parsed = readAppConfigFile(path);
|
|
6080
|
+
if (!parsed)
|
|
5943
6081
|
continue;
|
|
6082
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
6083
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
6084
|
+
}
|
|
6085
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
6086
|
+
if (new Set(values).size > 1)
|
|
6087
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
6088
|
+
for (const key of wanted) {
|
|
6089
|
+
if (parsed.unusable.has(key))
|
|
6090
|
+
return { key, value: "", path, unusable: true };
|
|
6091
|
+
const value = parsed.values.get(key)?.trim();
|
|
6092
|
+
if (value)
|
|
6093
|
+
return { key, value, path };
|
|
5944
6094
|
}
|
|
5945
|
-
const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
|
|
5946
|
-
kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
|
|
5947
6095
|
}
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
keptEntriesByEvent.add(m[1]);
|
|
6096
|
+
return null;
|
|
6097
|
+
}
|
|
6098
|
+
function assertUsableCredential(appName, source, value) {
|
|
6099
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
6100
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
5954
6101
|
}
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
` : ""),
|
|
5968
|
-
removed
|
|
6102
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
6103
|
+
return;
|
|
6104
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
6105
|
+
}
|
|
6106
|
+
function sealCredential(fields) {
|
|
6107
|
+
const { apiKey } = fields;
|
|
6108
|
+
const visible = {
|
|
6109
|
+
tier: fields.tier,
|
|
6110
|
+
source: fields.source,
|
|
6111
|
+
deliberate: fields.deliberate,
|
|
6112
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
6113
|
+
warning: fields.warning
|
|
5969
6114
|
};
|
|
6115
|
+
const sealed = { ...visible };
|
|
6116
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
6117
|
+
value: apiKey,
|
|
6118
|
+
enumerable: false,
|
|
6119
|
+
writable: false,
|
|
6120
|
+
configurable: false
|
|
6121
|
+
});
|
|
6122
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
6123
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
6124
|
+
value: fields.pointerVaultKey,
|
|
6125
|
+
enumerable: false,
|
|
6126
|
+
writable: false,
|
|
6127
|
+
configurable: false
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
6131
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
6132
|
+
enumerable: false,
|
|
6133
|
+
writable: false,
|
|
6134
|
+
configurable: false
|
|
6135
|
+
});
|
|
6136
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
6137
|
+
value: true,
|
|
6138
|
+
enumerable: false,
|
|
6139
|
+
writable: false,
|
|
6140
|
+
configurable: false
|
|
6141
|
+
});
|
|
6142
|
+
return Object.freeze(sealed);
|
|
6143
|
+
}
|
|
6144
|
+
function firstEnvValue(env, keys) {
|
|
6145
|
+
for (const key of keys) {
|
|
6146
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
6147
|
+
continue;
|
|
6148
|
+
const value = env[key]?.trim();
|
|
6149
|
+
if (value)
|
|
6150
|
+
return { key, value };
|
|
6151
|
+
}
|
|
6152
|
+
return null;
|
|
5970
6153
|
}
|
|
5971
|
-
function
|
|
5972
|
-
return
|
|
6154
|
+
function isAmbientEnvironment(env) {
|
|
6155
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
5973
6156
|
}
|
|
5974
|
-
function
|
|
5975
|
-
|
|
6157
|
+
function defaultKeychainRunner(argv) {
|
|
6158
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
6159
|
+
encoding: "utf8",
|
|
6160
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6161
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
6162
|
+
});
|
|
6163
|
+
return {
|
|
6164
|
+
status: result.status,
|
|
6165
|
+
stdout: result.stdout ?? "",
|
|
6166
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
6167
|
+
};
|
|
5976
6168
|
}
|
|
5977
|
-
function
|
|
5978
|
-
|
|
5979
|
-
if (!existsSync11(path))
|
|
6169
|
+
function keychainTierEnabled(env, options) {
|
|
6170
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
5980
6171
|
return false;
|
|
6172
|
+
if (options.enabled !== undefined)
|
|
6173
|
+
return options.enabled;
|
|
6174
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
6175
|
+
}
|
|
6176
|
+
function keychainAccount(env, options) {
|
|
6177
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
6178
|
+
if (station)
|
|
6179
|
+
return station;
|
|
6180
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
6181
|
+
if (host)
|
|
6182
|
+
return host;
|
|
6183
|
+
const user = env.USER?.trim();
|
|
6184
|
+
return user || null;
|
|
6185
|
+
}
|
|
6186
|
+
function keychainFailureHint(text) {
|
|
6187
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
6188
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
6189
|
+
return clean ? `: ${clean}` : "";
|
|
6190
|
+
}
|
|
6191
|
+
function readKeychainItem(name, env, kind, options) {
|
|
6192
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
6193
|
+
return null;
|
|
6194
|
+
const account = keychainAccount(env, options);
|
|
6195
|
+
if (!account)
|
|
6196
|
+
return null;
|
|
6197
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
6198
|
+
const source = `keychain:${service}@${account}`;
|
|
6199
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
6200
|
+
let result;
|
|
5981
6201
|
try {
|
|
5982
|
-
|
|
5983
|
-
} catch {
|
|
5984
|
-
|
|
6202
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
6203
|
+
} catch (error) {
|
|
6204
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
6205
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
6206
|
+
}
|
|
6207
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
6208
|
+
return null;
|
|
6209
|
+
if (result.status !== 0) {
|
|
6210
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
6211
|
+
}
|
|
6212
|
+
const value = result.stdout.trim();
|
|
6213
|
+
if (!value) {
|
|
6214
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
6215
|
+
}
|
|
6216
|
+
return { value, source };
|
|
6217
|
+
}
|
|
6218
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
6219
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
6220
|
+
}
|
|
6221
|
+
function snapshotClientEnvironment(name, env) {
|
|
6222
|
+
const keys = clientTransportEnvKeys(name);
|
|
6223
|
+
const ambient = isAmbientEnvironment(env);
|
|
6224
|
+
const snapshot = Object.create(null);
|
|
6225
|
+
for (const key of [
|
|
6226
|
+
...keys.apiUrlKeys,
|
|
6227
|
+
...keys.apiKeyKeys,
|
|
6228
|
+
credentialOverrideEnvKey(name),
|
|
6229
|
+
credentialPointerEnvKey(name),
|
|
6230
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
6231
|
+
"HOME",
|
|
6232
|
+
HASNA_HOME_ENV_KEY,
|
|
6233
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
6234
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
6235
|
+
"USER"
|
|
6236
|
+
]) {
|
|
6237
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
6238
|
+
if (!descriptor)
|
|
6239
|
+
continue;
|
|
6240
|
+
if (!("value" in descriptor)) {
|
|
6241
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
6242
|
+
}
|
|
6243
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
6244
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
6245
|
+
}
|
|
6246
|
+
snapshot[key] = descriptor.value;
|
|
6247
|
+
}
|
|
6248
|
+
if (ambient) {
|
|
6249
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
6250
|
+
value: true,
|
|
6251
|
+
enumerable: false,
|
|
6252
|
+
writable: false,
|
|
6253
|
+
configurable: false
|
|
6254
|
+
});
|
|
5985
6255
|
}
|
|
6256
|
+
return Object.freeze(snapshot);
|
|
5986
6257
|
}
|
|
5987
|
-
function
|
|
5988
|
-
|
|
5989
|
-
const
|
|
5990
|
-
const
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
return {
|
|
6258
|
+
function resolveCredential(name, env, options = {}) {
|
|
6259
|
+
env = snapshotClientEnvironment(name, env);
|
|
6260
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
6261
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
6262
|
+
if (options.apiKey !== undefined) {
|
|
6263
|
+
const explicitKey = options.apiKey.trim();
|
|
6264
|
+
if (!explicitKey) {
|
|
6265
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
6266
|
+
}
|
|
6267
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
6268
|
+
return sealCredential({
|
|
6269
|
+
apiKey: explicitKey,
|
|
6270
|
+
tier: "argument",
|
|
6271
|
+
source: "explicit apiKey argument",
|
|
6272
|
+
deliberate: true,
|
|
6273
|
+
diskCandidates: diskPaths,
|
|
6274
|
+
warning: null
|
|
6275
|
+
});
|
|
5998
6276
|
}
|
|
5999
|
-
const
|
|
6000
|
-
const
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
if (!scopes.includes(s))
|
|
6006
|
-
scopes.push(s);
|
|
6007
|
-
}
|
|
6277
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
6278
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
6279
|
+
if (overrideRaw !== undefined) {
|
|
6280
|
+
const override = overrideRaw.trim();
|
|
6281
|
+
if (!override) {
|
|
6282
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
6008
6283
|
}
|
|
6284
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
6285
|
+
return sealCredential({
|
|
6286
|
+
apiKey: override,
|
|
6287
|
+
tier: "override",
|
|
6288
|
+
source: overrideKeyName,
|
|
6289
|
+
deliberate: true,
|
|
6290
|
+
diskCandidates: diskPaths,
|
|
6291
|
+
warning: null
|
|
6292
|
+
});
|
|
6009
6293
|
}
|
|
6010
|
-
const
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
6020
|
-
registrationsRemaining.push("codewith");
|
|
6021
|
-
}
|
|
6294
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
6295
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
6296
|
+
if (pointerRaw !== undefined) {
|
|
6297
|
+
const pointer = pointerRaw.trim();
|
|
6298
|
+
if (!pointer) {
|
|
6299
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
6300
|
+
}
|
|
6301
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
6302
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
6022
6303
|
}
|
|
6304
|
+
return sealCredential({
|
|
6305
|
+
apiKey: "",
|
|
6306
|
+
pointerVaultKey: pointer,
|
|
6307
|
+
tier: "pointer",
|
|
6308
|
+
source: pointerKeyName,
|
|
6309
|
+
deliberate: true,
|
|
6310
|
+
diskCandidates: diskPaths,
|
|
6311
|
+
warning: null
|
|
6312
|
+
});
|
|
6023
6313
|
}
|
|
6024
|
-
if (
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6314
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
6315
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
6316
|
+
}
|
|
6317
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
6318
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
6319
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
6320
|
+
}
|
|
6321
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
6322
|
+
if (profile) {
|
|
6323
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
6324
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
6325
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
6326
|
+
}
|
|
6327
|
+
const paths = profileDiskSources(name, env, profile);
|
|
6328
|
+
for (const path of paths) {
|
|
6329
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
6330
|
+
if (value) {
|
|
6331
|
+
assertUsableCredential(name, path, value);
|
|
6332
|
+
return sealCredential({
|
|
6333
|
+
apiKey: value,
|
|
6334
|
+
tier: "profile",
|
|
6335
|
+
source: path,
|
|
6336
|
+
deliberate: true,
|
|
6337
|
+
diskCandidates: paths,
|
|
6338
|
+
warning: null
|
|
6339
|
+
});
|
|
6340
|
+
}
|
|
6039
6341
|
}
|
|
6342
|
+
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);
|
|
6040
6343
|
}
|
|
6041
|
-
const
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6344
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
6345
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
6346
|
+
if (blankEnv) {
|
|
6347
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
6348
|
+
}
|
|
6349
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
6350
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
6351
|
+
}
|
|
6352
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
6353
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
6354
|
+
if (keychainHit) {
|
|
6355
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
6356
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain 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;
|
|
6357
|
+
return sealCredential({
|
|
6358
|
+
apiKey: keychainHit.value,
|
|
6359
|
+
tier: "keychain",
|
|
6360
|
+
source: keychainHit.source,
|
|
6361
|
+
deliberate: false,
|
|
6362
|
+
diskCandidates: diskPaths,
|
|
6363
|
+
warning
|
|
6364
|
+
});
|
|
6365
|
+
}
|
|
6366
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
6367
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
6368
|
+
if (diskHits.length > 0) {
|
|
6369
|
+
const winner = diskHits[0];
|
|
6370
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
6371
|
+
const divergentSources = [
|
|
6372
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
6373
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
6374
|
+
];
|
|
6375
|
+
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;
|
|
6376
|
+
return sealCredential({
|
|
6377
|
+
apiKey: winner.value,
|
|
6378
|
+
tier: winner.src.tier,
|
|
6379
|
+
source: winner.src.path,
|
|
6380
|
+
deliberate: false,
|
|
6381
|
+
diskCandidates: diskPaths,
|
|
6382
|
+
warning
|
|
6383
|
+
});
|
|
6384
|
+
}
|
|
6385
|
+
if (envHit) {
|
|
6386
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
6387
|
+
return sealCredential({
|
|
6388
|
+
apiKey: envHit.value,
|
|
6389
|
+
tier: "env",
|
|
6390
|
+
source: envHit.key,
|
|
6391
|
+
deliberate: false,
|
|
6392
|
+
diskCandidates: diskPaths,
|
|
6393
|
+
warning: null
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
return null;
|
|
6052
6397
|
}
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
init_store();
|
|
6059
|
-
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
6060
|
-
HOOKS_DIR = existsSync11(join13(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join13(__dirname2, "..", "..", "hooks") : join13(__dirname2, "..", "hooks");
|
|
6061
|
-
EVENT_MAP = {
|
|
6062
|
-
claude: {
|
|
6063
|
-
PreToolUse: "PreToolUse",
|
|
6064
|
-
PostToolUse: "PostToolUse",
|
|
6065
|
-
Stop: "Stop",
|
|
6066
|
-
Notification: "Notification",
|
|
6067
|
-
SessionStart: "SessionStart",
|
|
6068
|
-
SessionEnd: "SessionEnd",
|
|
6069
|
-
UserPromptSubmit: null,
|
|
6070
|
-
SubagentStart: null
|
|
6071
|
-
},
|
|
6072
|
-
gemini: {
|
|
6073
|
-
PreToolUse: "BeforeTool",
|
|
6074
|
-
PostToolUse: "AfterTool",
|
|
6075
|
-
Stop: "AfterAgent",
|
|
6076
|
-
Notification: "Notification",
|
|
6077
|
-
SessionStart: null,
|
|
6078
|
-
SessionEnd: null,
|
|
6079
|
-
UserPromptSubmit: null,
|
|
6080
|
-
SubagentStart: null
|
|
6081
|
-
},
|
|
6082
|
-
codewith: {
|
|
6083
|
-
PreToolUse: "PreToolUse",
|
|
6084
|
-
PostToolUse: "PostToolUse",
|
|
6085
|
-
Stop: "Stop",
|
|
6086
|
-
Notification: null,
|
|
6087
|
-
SessionStart: "SessionStart",
|
|
6088
|
-
SessionEnd: null,
|
|
6089
|
-
UserPromptSubmit: "UserPromptSubmit",
|
|
6090
|
-
SubagentStart: "SubagentStart"
|
|
6091
|
-
}
|
|
6092
|
-
};
|
|
6093
|
-
getInstalledHooks = getRegisteredHooks;
|
|
6094
|
-
});
|
|
6095
|
-
|
|
6096
|
-
// src/lib/registration.ts
|
|
6097
|
-
function safeMatcherTest(pattern, value) {
|
|
6098
|
-
try {
|
|
6099
|
-
return new RegExp(pattern).test(value);
|
|
6100
|
-
} catch {
|
|
6398
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
6399
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
6400
|
+
}
|
|
6401
|
+
function isValidDnsDomain(value) {
|
|
6402
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
6101
6403
|
return false;
|
|
6102
6404
|
}
|
|
6405
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
6103
6406
|
}
|
|
6104
|
-
function
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
return 0;
|
|
6108
|
-
let count = 0;
|
|
6109
|
-
for (const eventKey of Object.keys(hooks)) {
|
|
6110
|
-
const entries = hooks[eventKey];
|
|
6111
|
-
if (!Array.isArray(entries))
|
|
6112
|
-
continue;
|
|
6113
|
-
for (const entry of entries) {
|
|
6114
|
-
if (Array.isArray(entry?.hooks))
|
|
6115
|
-
count += entry.hooks.length;
|
|
6116
|
-
}
|
|
6407
|
+
function validateAppSlug(name) {
|
|
6408
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
6409
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
6117
6410
|
}
|
|
6118
|
-
return
|
|
6411
|
+
return name;
|
|
6119
6412
|
}
|
|
6120
|
-
function
|
|
6121
|
-
const
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
const
|
|
6125
|
-
const
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
if (!match || match[1] !== name)
|
|
6131
|
-
return false;
|
|
6132
|
-
if (!matcher)
|
|
6133
|
-
return true;
|
|
6134
|
-
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
6135
|
-
if (!entryMatcher)
|
|
6136
|
-
return true;
|
|
6137
|
-
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
6138
|
-
}));
|
|
6413
|
+
function rawAuthority(value) {
|
|
6414
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
6415
|
+
if (!match)
|
|
6416
|
+
throw new Error("API URL must be absolute.");
|
|
6417
|
+
const afterScheme = value.slice(match[0].length);
|
|
6418
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
6419
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
6420
|
+
if (!authority)
|
|
6421
|
+
throw new Error("API URL must include a hostname.");
|
|
6422
|
+
return authority;
|
|
6139
6423
|
}
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6424
|
+
function assertCanonicalPort(port) {
|
|
6425
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
6426
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
6427
|
+
}
|
|
6428
|
+
const numericPort = Number(port);
|
|
6429
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
6430
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
6146
6431
|
}
|
|
6147
|
-
return out;
|
|
6148
6432
|
}
|
|
6149
|
-
function
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6433
|
+
function canonicalAuthorityHostname(authority) {
|
|
6434
|
+
let rawHostname;
|
|
6435
|
+
if (authority.startsWith("[")) {
|
|
6436
|
+
const closingBracket = authority.indexOf("]");
|
|
6437
|
+
if (closingBracket === -1) {
|
|
6438
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
6439
|
+
}
|
|
6440
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
6441
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
6442
|
+
if (portSuffix) {
|
|
6443
|
+
if (!portSuffix.startsWith(":")) {
|
|
6444
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
6445
|
+
}
|
|
6446
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
6447
|
+
}
|
|
6448
|
+
if (isIP2(rawHostname.slice(1, -1)) !== 6) {
|
|
6449
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
6450
|
+
}
|
|
6451
|
+
} else {
|
|
6452
|
+
const firstColon = authority.indexOf(":");
|
|
6453
|
+
const lastColon = authority.lastIndexOf(":");
|
|
6454
|
+
if (firstColon !== lastColon) {
|
|
6455
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
6456
|
+
}
|
|
6457
|
+
if (lastColon !== -1) {
|
|
6458
|
+
const port = authority.slice(lastColon + 1);
|
|
6459
|
+
assertCanonicalPort(port);
|
|
6460
|
+
rawHostname = authority.slice(0, lastColon);
|
|
6461
|
+
} else {
|
|
6462
|
+
rawHostname = authority;
|
|
6463
|
+
}
|
|
6464
|
+
const ipVersion = isIP2(rawHostname);
|
|
6465
|
+
const numericAddressParts = rawHostname.split(".");
|
|
6466
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
6467
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
6468
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
6158
6469
|
}
|
|
6159
|
-
return out;
|
|
6160
6470
|
}
|
|
6161
|
-
return
|
|
6471
|
+
return rawHostname.toLowerCase();
|
|
6162
6472
|
}
|
|
6163
|
-
function
|
|
6164
|
-
|
|
6165
|
-
return null;
|
|
6166
|
-
try {
|
|
6167
|
-
const parsed = JSON.parse(text);
|
|
6168
|
-
return redactText(JSON.stringify(redactValue2(parsed)));
|
|
6169
|
-
} catch {
|
|
6170
|
-
return redactText(text);
|
|
6171
|
-
}
|
|
6473
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
6474
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
6172
6475
|
}
|
|
6173
|
-
function
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
const value = out[field];
|
|
6177
|
-
if (typeof value === "string")
|
|
6178
|
-
out[field] = redactEventPayload(value);
|
|
6179
|
-
else if (value === null)
|
|
6180
|
-
out[field] = null;
|
|
6476
|
+
function toV1BaseUrl(apiUrl) {
|
|
6477
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
6478
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
6181
6479
|
}
|
|
6182
|
-
|
|
6480
|
+
const input = apiUrl.trim();
|
|
6481
|
+
const authority = rawAuthority(input);
|
|
6482
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
6483
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
6484
|
+
}
|
|
6485
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
6486
|
+
const url = new URL(input);
|
|
6487
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
6488
|
+
throw new Error("API URL must use http or https.");
|
|
6489
|
+
}
|
|
6490
|
+
if (url.username || url.password) {
|
|
6491
|
+
throw new Error("API URL must not include credentials.");
|
|
6492
|
+
}
|
|
6493
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
6494
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
6495
|
+
}
|
|
6496
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
6497
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
6498
|
+
}
|
|
6499
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
6500
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
6501
|
+
}
|
|
6502
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
6503
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
6504
|
+
}
|
|
6505
|
+
if (url.search || url.hash) {
|
|
6506
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
6507
|
+
}
|
|
6508
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
6509
|
+
if (path.endsWith("/v1"))
|
|
6510
|
+
path = path.slice(0, -"/v1".length);
|
|
6511
|
+
url.pathname = `${path}/v1`;
|
|
6512
|
+
return url.toString().replace(/\/+$/, "");
|
|
6183
6513
|
}
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6514
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
6515
|
+
env = snapshotClientEnvironment(name, env);
|
|
6516
|
+
const keys = clientTransportEnvKeys(name);
|
|
6517
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
6518
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
6519
|
+
if (blankUrl) {
|
|
6520
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
6521
|
+
}
|
|
6522
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
6523
|
+
if (controlledUrl) {
|
|
6524
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
6525
|
+
}
|
|
6526
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
6527
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
6528
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
6529
|
+
}
|
|
6530
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
6531
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
6532
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
6533
|
+
if (diskConfigUrlHit?.unusable) {
|
|
6534
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
6535
|
+
}
|
|
6536
|
+
const urlCandidates = [
|
|
6537
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
6538
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
6539
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
6203
6540
|
];
|
|
6541
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
6542
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
6543
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
6544
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
6545
|
+
}
|
|
6546
|
+
const warnings = [];
|
|
6547
|
+
if (configuredUrl && !envUrlHit) {
|
|
6548
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
6549
|
+
}
|
|
6550
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
6551
|
+
if (!credential) {
|
|
6552
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
6553
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
6554
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
6555
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
6556
|
+
}
|
|
6557
|
+
if (credential.warning)
|
|
6558
|
+
warnings.push(credential.warning);
|
|
6559
|
+
let urlHit;
|
|
6560
|
+
if (configuredUrl) {
|
|
6561
|
+
urlHit = configuredUrl;
|
|
6562
|
+
} else {
|
|
6563
|
+
try {
|
|
6564
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
6565
|
+
} catch (error) {
|
|
6566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6567
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
const apiUrlSource = urlHit.key;
|
|
6571
|
+
let baseUrl;
|
|
6572
|
+
try {
|
|
6573
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
6574
|
+
} catch (error) {
|
|
6575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6576
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
6577
|
+
}
|
|
6578
|
+
return {
|
|
6579
|
+
resolution: {
|
|
6580
|
+
transport: "http",
|
|
6581
|
+
transportSource: urlHit.key,
|
|
6582
|
+
baseUrl,
|
|
6583
|
+
apiUrlSource,
|
|
6584
|
+
apiKeyPresent: true,
|
|
6585
|
+
apiKeySource: credential.source,
|
|
6586
|
+
apiKeyTier: credential.tier,
|
|
6587
|
+
misconfigured: false,
|
|
6588
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
6589
|
+
},
|
|
6590
|
+
credential
|
|
6591
|
+
};
|
|
6592
|
+
}
|
|
6593
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
6594
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
6595
|
+
}
|
|
6596
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
6597
|
+
const paths = credentialDiskSources(name, env);
|
|
6598
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
6599
|
+
}
|
|
6600
|
+
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, SECRETS_PACKAGE_SPECIFIER, 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;
|
|
6601
|
+
var init_transport = __esm(() => {
|
|
6602
|
+
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
6603
|
+
appName;
|
|
6604
|
+
attempted;
|
|
6605
|
+
constructor(appName, message, attempted) {
|
|
6606
|
+
super(message);
|
|
6607
|
+
this.name = "CredentialResolutionError";
|
|
6608
|
+
this.appName = appName;
|
|
6609
|
+
this.attempted = attempted;
|
|
6610
|
+
}
|
|
6611
|
+
};
|
|
6612
|
+
CredentialFileUnsafeError = class CredentialFileUnsafeError extends Error {
|
|
6613
|
+
path;
|
|
6614
|
+
constructor(path, reason) {
|
|
6615
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
6616
|
+
this.name = "CredentialFileUnsafeError";
|
|
6617
|
+
this.path = path;
|
|
6618
|
+
}
|
|
6619
|
+
};
|
|
6620
|
+
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
6621
|
+
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6622
|
+
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
6623
|
+
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
6624
|
+
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
6625
|
+
CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
6626
|
+
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
6627
|
+
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
6628
|
+
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
6629
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
6630
|
+
requireSecretsSdk = createRequire(import.meta.url);
|
|
6631
|
+
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
6632
|
+
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6633
|
+
ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
|
|
6634
|
+
appName;
|
|
6635
|
+
sources;
|
|
6636
|
+
constructor(appName, message, sources = []) {
|
|
6637
|
+
super(message);
|
|
6638
|
+
this.name = "ClientTransportConfigurationError";
|
|
6639
|
+
this.appName = appName;
|
|
6640
|
+
this.sources = Object.freeze([...sources]);
|
|
6641
|
+
}
|
|
6642
|
+
};
|
|
6643
|
+
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
6644
|
+
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
6645
|
+
"host",
|
|
6646
|
+
":authority",
|
|
6647
|
+
"forwarded",
|
|
6648
|
+
"x-forwarded-host",
|
|
6649
|
+
"x-original-host"
|
|
6650
|
+
]);
|
|
6204
6651
|
});
|
|
6205
6652
|
|
|
6206
|
-
// src/lib/
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
function resolveProfilesDir() {
|
|
6210
|
-
const newDir = join14(getEffectiveDataRoot(), "profiles");
|
|
6211
|
-
const oldDir = join14(getHomeDir(), ".hooks", "profiles");
|
|
6212
|
-
if (!existsSync12(newDir) && existsSync12(oldDir)) {
|
|
6213
|
-
mkdirSync6(newDir, { recursive: true });
|
|
6214
|
-
cpSync2(oldDir, newDir, { recursive: true });
|
|
6215
|
-
}
|
|
6216
|
-
return newDir;
|
|
6653
|
+
// src/lib/local-opt-in.ts
|
|
6654
|
+
function isHooksLocalOptIn(env = process.env) {
|
|
6655
|
+
return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
6217
6656
|
}
|
|
6218
|
-
function
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6657
|
+
function hooksAuthorityEnvKeys() {
|
|
6658
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
6659
|
+
return [
|
|
6660
|
+
...keys.apiUrlKeys,
|
|
6661
|
+
...keys.apiKeyKeys,
|
|
6662
|
+
credentialOverrideEnvKey("hooks"),
|
|
6663
|
+
credentialPointerEnvKey("hooks"),
|
|
6664
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
6665
|
+
];
|
|
6222
6666
|
}
|
|
6223
|
-
function
|
|
6224
|
-
return
|
|
6667
|
+
function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
6668
|
+
return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
6225
6669
|
}
|
|
6226
|
-
function
|
|
6227
|
-
return
|
|
6670
|
+
function selectsHooksLocalStore(env = process.env) {
|
|
6671
|
+
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
6228
6672
|
}
|
|
6229
|
-
function
|
|
6230
|
-
|
|
6673
|
+
function hooksResolverEnv(env) {
|
|
6674
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
6675
|
+
if (blanks.length === 0)
|
|
6676
|
+
return env;
|
|
6677
|
+
const next = { ...env };
|
|
6678
|
+
for (const key of blanks)
|
|
6679
|
+
delete next[key];
|
|
6680
|
+
return next;
|
|
6231
6681
|
}
|
|
6232
|
-
function
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
const profile = {
|
|
6237
|
-
agent_id: id,
|
|
6238
|
-
agent_type: input.agent_type,
|
|
6239
|
-
created_at: now2,
|
|
6240
|
-
last_seen_at: now2,
|
|
6241
|
-
preferences: {}
|
|
6242
|
-
};
|
|
6243
|
-
if (input.name) {
|
|
6244
|
-
profile.name = input.name;
|
|
6245
|
-
}
|
|
6246
|
-
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
6247
|
-
`);
|
|
6248
|
-
return profile;
|
|
6682
|
+
function isAmbientHooksEnv(env) {
|
|
6683
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
6684
|
+
return true;
|
|
6685
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
6249
6686
|
}
|
|
6250
|
-
function
|
|
6251
|
-
const
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
return null;
|
|
6687
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
6688
|
+
const normalised = hooksResolverEnv(env);
|
|
6689
|
+
if (normalised === env)
|
|
6690
|
+
return { env: normalised, credentials };
|
|
6691
|
+
const keychain = { ...credentials.keychain };
|
|
6692
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
6693
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
6258
6694
|
}
|
|
6695
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
6259
6696
|
}
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6697
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6698
|
+
var init_local_opt_in = __esm(() => {
|
|
6699
|
+
init_transport();
|
|
6700
|
+
HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
6701
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
6702
|
+
});
|
|
6703
|
+
|
|
6704
|
+
// src/lib/transport.ts
|
|
6705
|
+
var exports_transport = {};
|
|
6706
|
+
__export(exports_transport, {
|
|
6707
|
+
rethrowHooksAuthorityFailure: () => rethrowHooksAuthorityFailure,
|
|
6708
|
+
resolveHooksTransport: () => resolveHooksTransport,
|
|
6709
|
+
resolveHooksServePublishKey: () => resolveHooksServePublishKey,
|
|
6710
|
+
hooksRegistryOrigin: () => hooksRegistryOrigin,
|
|
6711
|
+
announceHooksLocalMode: () => announceHooksLocalMode,
|
|
6712
|
+
__resetHooksLocalNotice: () => __resetHooksLocalNotice
|
|
6713
|
+
});
|
|
6714
|
+
function __resetHooksLocalNotice() {
|
|
6715
|
+
localNoticePrinted = false;
|
|
6276
6716
|
}
|
|
6277
|
-
function
|
|
6278
|
-
|
|
6279
|
-
if (!profile)
|
|
6717
|
+
function announceLocal(notice, reason) {
|
|
6718
|
+
if (localNoticePrinted)
|
|
6280
6719
|
return;
|
|
6281
|
-
|
|
6282
|
-
|
|
6720
|
+
localNoticePrinted = true;
|
|
6721
|
+
const line = `hooks: LOCAL mode \u2014 ${reason}; using the bundled registry and the local store at ` + `${getHooksDataDir()}, not a remote registry. Set HASNA_HOOKS_API_KEY (or the Keychain item ` + `hasna.credentials.hooks.api-key / ~/.hasna/hooks/config/credentials) to go remote.`;
|
|
6722
|
+
if (notice)
|
|
6723
|
+
notice(line);
|
|
6724
|
+
else if (typeof process !== "undefined")
|
|
6725
|
+
process.stderr.write(`${line}
|
|
6283
6726
|
`);
|
|
6284
6727
|
}
|
|
6285
|
-
function
|
|
6286
|
-
|
|
6728
|
+
function announceHooksLocalMode(reason = "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority", notice) {
|
|
6729
|
+
announceLocal(notice, reason);
|
|
6287
6730
|
}
|
|
6288
|
-
function
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
if (
|
|
6299
|
-
|
|
6300
|
-
continue;
|
|
6731
|
+
function hooksRegistryOrigin(v1BaseUrl) {
|
|
6732
|
+
return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
6733
|
+
}
|
|
6734
|
+
function rethrowHooksAuthorityFailure(error) {
|
|
6735
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6736
|
+
const name = error instanceof Error ? error.name : "";
|
|
6737
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
6738
|
+
throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
6739
|
+
}
|
|
6740
|
+
if (/no API key could be resolved/.test(message)) {
|
|
6741
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
6742
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved from the Keychain item " + `hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1, alias HOOKS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
|
|
6301
6743
|
}
|
|
6302
|
-
|
|
6303
|
-
`);
|
|
6304
|
-
imported++;
|
|
6744
|
+
throw new Error("REMOTE_API_KEY_MISSING: the remote hooks registry requires HASNA_HOOKS_API_KEY, the Keychain item " + `hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
6305
6745
|
}
|
|
6306
|
-
|
|
6746
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
6307
6747
|
}
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6748
|
+
function resolveHooksTransport(env = process.env, options = {}) {
|
|
6749
|
+
if (selectsHooksLocalStore(env)) {
|
|
6750
|
+
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
6751
|
+
return { mode: "local", source: "local-opt-in", authority: null };
|
|
6752
|
+
}
|
|
6753
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
6754
|
+
const tier1 = options.credentials?.apiKey;
|
|
6755
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
6756
|
+
let resolution;
|
|
6757
|
+
let credential;
|
|
6758
|
+
try {
|
|
6759
|
+
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
6760
|
+
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
6761
|
+
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
6762
|
+
});
|
|
6763
|
+
} catch (error) {
|
|
6764
|
+
rethrowHooksAuthorityFailure(error);
|
|
6765
|
+
}
|
|
6766
|
+
if (!credential) {
|
|
6767
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
6768
|
+
}
|
|
6769
|
+
return {
|
|
6770
|
+
mode: "remote",
|
|
6771
|
+
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
6772
|
+
authority: {
|
|
6773
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
6774
|
+
apiKey: credential.apiKey,
|
|
6775
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
6776
|
+
apiKeySource: credential.source,
|
|
6777
|
+
apiKeyTier: credential.tier,
|
|
6778
|
+
v1BaseUrl: resolution.baseUrl,
|
|
6779
|
+
warning: resolution.warning
|
|
6780
|
+
}
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
6784
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
6785
|
+
const tier1 = options.credentials?.apiKey;
|
|
6786
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
6787
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
6788
|
+
}
|
|
6789
|
+
var localNoticePrinted = false;
|
|
6790
|
+
var init_transport2 = __esm(() => {
|
|
6791
|
+
init_transport();
|
|
6792
|
+
init_config();
|
|
6793
|
+
init_local_opt_in();
|
|
6312
6794
|
});
|
|
6313
6795
|
|
|
6314
6796
|
// src/lib/db-writer.ts
|
|
@@ -6408,8 +6890,8 @@ var init_db_writer = __esm(() => {
|
|
|
6408
6890
|
});
|
|
6409
6891
|
|
|
6410
6892
|
// src/lib/hook-env.ts
|
|
6411
|
-
import { dirname as dirname4, isAbsolute as
|
|
6412
|
-
import { existsSync as
|
|
6893
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
6894
|
+
import { existsSync as existsSync12, statSync } from "fs";
|
|
6413
6895
|
function isDeniedEnvName(name) {
|
|
6414
6896
|
const upper = name.toUpperCase();
|
|
6415
6897
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -6427,7 +6909,7 @@ function isBashFunctionEnvName(name) {
|
|
|
6427
6909
|
return name.startsWith("BASH_FUNC_");
|
|
6428
6910
|
}
|
|
6429
6911
|
function isUnsafePathEntry(entry, home) {
|
|
6430
|
-
if (entry === "" || !
|
|
6912
|
+
if (entry === "" || !isAbsolute3(entry))
|
|
6431
6913
|
return true;
|
|
6432
6914
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
6433
6915
|
return true;
|
|
@@ -6436,14 +6918,14 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6436
6918
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
6437
6919
|
return true;
|
|
6438
6920
|
try {
|
|
6439
|
-
if ((
|
|
6921
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
6440
6922
|
return true;
|
|
6441
6923
|
} catch {}
|
|
6442
6924
|
return false;
|
|
6443
6925
|
}
|
|
6444
6926
|
function sanitizeHookPath(pathValue, home) {
|
|
6445
6927
|
const baseline = PATH_BASELINE.slice();
|
|
6446
|
-
if (process.platform === "darwin" &&
|
|
6928
|
+
if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
|
|
6447
6929
|
baseline.push("/opt/homebrew/bin");
|
|
6448
6930
|
const bunDir = dirname4(process.execPath);
|
|
6449
6931
|
const seen = new Set;
|
|
@@ -6568,8 +7050,8 @@ __export(exports_run, {
|
|
|
6568
7050
|
HookTimeoutError: () => HookTimeoutError
|
|
6569
7051
|
});
|
|
6570
7052
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6571
|
-
import { rmSync as
|
|
6572
|
-
import { basename as basename2, dirname as dirname5, isAbsolute as
|
|
7053
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
7054
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join13 } from "path";
|
|
6573
7055
|
function extensionOf(scriptPath) {
|
|
6574
7056
|
const base = basename2(scriptPath);
|
|
6575
7057
|
const dot = base.lastIndexOf(".");
|
|
@@ -6640,7 +7122,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6640
7122
|
async function executeVerifiedScript(options) {
|
|
6641
7123
|
const scriptDir = dirname5(options.scriptPath);
|
|
6642
7124
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6643
|
-
const tempPath =
|
|
7125
|
+
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6644
7126
|
let proc = null;
|
|
6645
7127
|
let timedOut = false;
|
|
6646
7128
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6648,8 +7130,8 @@ async function executeVerifiedScript(options) {
|
|
|
6648
7130
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
6649
7131
|
}, options.timeout) : null;
|
|
6650
7132
|
try {
|
|
6651
|
-
|
|
6652
|
-
proc = Bun.spawn([...interpreter.command,
|
|
7133
|
+
writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
7134
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
6653
7135
|
stdin: new Response(options.stdin),
|
|
6654
7136
|
stdout: "pipe",
|
|
6655
7137
|
stderr: "pipe",
|
|
@@ -6670,7 +7152,7 @@ async function executeVerifiedScript(options) {
|
|
|
6670
7152
|
if (timeoutTimer)
|
|
6671
7153
|
clearTimeout(timeoutTimer);
|
|
6672
7154
|
try {
|
|
6673
|
-
|
|
7155
|
+
rmSync2(tempPath, { force: true });
|
|
6674
7156
|
} catch {}
|
|
6675
7157
|
}
|
|
6676
7158
|
}
|
|
@@ -6731,15 +7213,15 @@ __export(exports_custom_install, {
|
|
|
6731
7213
|
installCustomSource: () => installCustomSource,
|
|
6732
7214
|
ensureCustomDir: () => ensureCustomDir
|
|
6733
7215
|
});
|
|
6734
|
-
import { existsSync as
|
|
6735
|
-
import { join as
|
|
7216
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
7217
|
+
import { join as join14 } from "path";
|
|
6736
7218
|
import { tmpdir } from "os";
|
|
6737
7219
|
function manifestAt(dir) {
|
|
6738
|
-
const path =
|
|
6739
|
-
if (!
|
|
7220
|
+
const path = join14(dir, "manifest.json");
|
|
7221
|
+
if (!existsSync13(path))
|
|
6740
7222
|
return;
|
|
6741
7223
|
try {
|
|
6742
|
-
return parseManifest(
|
|
7224
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
6743
7225
|
} catch {
|
|
6744
7226
|
return;
|
|
6745
7227
|
}
|
|
@@ -6758,8 +7240,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
6758
7240
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
6759
7241
|
}
|
|
6760
7242
|
async function cloneToTemp(gitUrl) {
|
|
6761
|
-
const tmp = mkdtempSync(
|
|
6762
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
7243
|
+
const tmp = mkdtempSync(join14(tmpdir(), "hooks-install-"));
|
|
7244
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
|
|
6763
7245
|
stdout: "pipe",
|
|
6764
7246
|
stderr: "pipe"
|
|
6765
7247
|
});
|
|
@@ -6769,10 +7251,10 @@ async function cloneToTemp(gitUrl) {
|
|
|
6769
7251
|
proc.exited
|
|
6770
7252
|
]);
|
|
6771
7253
|
if (exitCode !== 0) {
|
|
6772
|
-
|
|
7254
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
6773
7255
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6774
7256
|
}
|
|
6775
|
-
return
|
|
7257
|
+
return join14(tmp, "repo");
|
|
6776
7258
|
}
|
|
6777
7259
|
function isGitUrl(value) {
|
|
6778
7260
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -6782,7 +7264,7 @@ function isCustomSource(value) {
|
|
|
6782
7264
|
return true;
|
|
6783
7265
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
6784
7266
|
return true;
|
|
6785
|
-
return
|
|
7267
|
+
return existsSync13(value);
|
|
6786
7268
|
}
|
|
6787
7269
|
async function installCustomSource(source) {
|
|
6788
7270
|
if (isGitUrl(source)) {
|
|
@@ -6790,17 +7272,17 @@ async function installCustomSource(source) {
|
|
|
6790
7272
|
try {
|
|
6791
7273
|
return copyManifestDir(repoDir, source, "git");
|
|
6792
7274
|
} finally {
|
|
6793
|
-
|
|
7275
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
6794
7276
|
}
|
|
6795
7277
|
}
|
|
6796
7278
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
6797
7279
|
return installManifestUrl(source);
|
|
6798
7280
|
}
|
|
6799
|
-
if (!
|
|
7281
|
+
if (!existsSync13(source)) {
|
|
6800
7282
|
throw new Error(`No such local path: ${source}`);
|
|
6801
7283
|
}
|
|
6802
7284
|
if (source.endsWith("manifest.json")) {
|
|
6803
|
-
const manifest = parseManifest(
|
|
7285
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
6804
7286
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
6805
7287
|
}
|
|
6806
7288
|
return copyManifestDir(source, source, "local");
|
|
@@ -6831,7 +7313,7 @@ async function installManifestUrl(url) {
|
|
|
6831
7313
|
}
|
|
6832
7314
|
function ensureCustomDir() {
|
|
6833
7315
|
const dir = getCustomHooksDir();
|
|
6834
|
-
|
|
7316
|
+
mkdirSync6(dir, { recursive: true });
|
|
6835
7317
|
return dir;
|
|
6836
7318
|
}
|
|
6837
7319
|
var init_custom_install = __esm(() => {
|
|
@@ -6849,12 +7331,12 @@ __export(exports_sync, {
|
|
|
6849
7331
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6850
7332
|
});
|
|
6851
7333
|
import { createHash as createHash2 } from "crypto";
|
|
6852
|
-
import { readFileSync as
|
|
7334
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6853
7335
|
function sha256OfText(text) {
|
|
6854
7336
|
return createHash2("sha256").update(text).digest("hex");
|
|
6855
7337
|
}
|
|
6856
7338
|
function sha256OfPath(path) {
|
|
6857
|
-
return createHash2("sha256").update(
|
|
7339
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6858
7340
|
}
|
|
6859
7341
|
function collectBundledCatalog() {
|
|
6860
7342
|
const out = [];
|
|
@@ -6870,9 +7352,8 @@ function collectBundledCatalog() {
|
|
|
6870
7352
|
}
|
|
6871
7353
|
return out;
|
|
6872
7354
|
}
|
|
6873
|
-
async function fetchJson(base, path, opts) {
|
|
7355
|
+
async function fetchJson(base, path, apiKey, opts) {
|
|
6874
7356
|
const headers = { accept: "application/json" };
|
|
6875
|
-
const apiKey = resolveApiKey();
|
|
6876
7357
|
if (apiKey)
|
|
6877
7358
|
headers["x-api-key"] = apiKey;
|
|
6878
7359
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -6881,7 +7362,7 @@ async function fetchJson(base, path, opts) {
|
|
|
6881
7362
|
signal: AbortSignal.timeout(15000)
|
|
6882
7363
|
});
|
|
6883
7364
|
if (res.status === 401) {
|
|
6884
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or
|
|
7365
|
+
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, " + "or ~/.hasna/hooks/config/credentials (resolved per call, never stored)");
|
|
6885
7366
|
}
|
|
6886
7367
|
if (!res.ok) {
|
|
6887
7368
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
@@ -6892,9 +7373,9 @@ async function fetchJson(base, path, opts) {
|
|
|
6892
7373
|
}
|
|
6893
7374
|
return body;
|
|
6894
7375
|
}
|
|
6895
|
-
async function fetchRemoteState(apiUrl) {
|
|
6896
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6897
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
7376
|
+
async function fetchRemoteState(apiUrl, apiKey) {
|
|
7377
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
|
|
7378
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
6898
7379
|
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6899
7380
|
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6900
7381
|
}
|
|
@@ -6937,20 +7418,25 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6937
7418
|
return diff;
|
|
6938
7419
|
}
|
|
6939
7420
|
async function planSync(options = {}) {
|
|
6940
|
-
const
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
7421
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
7422
|
+
credentials: options.credentials
|
|
7423
|
+
});
|
|
7424
|
+
if (transport.mode === "remote" && transport.authority) {
|
|
7425
|
+
const { origin, apiKey } = transport.authority;
|
|
7426
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
7427
|
+
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6944
7428
|
}
|
|
6945
7429
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6946
7430
|
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6947
7431
|
}
|
|
6948
7432
|
async function syncHooks(options = {}) {
|
|
6949
|
-
const
|
|
6950
|
-
|
|
7433
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
7434
|
+
credentials: options.credentials
|
|
7435
|
+
});
|
|
7436
|
+
const plan = await planSync(options);
|
|
6951
7437
|
if (options.dryRun)
|
|
6952
7438
|
return { ...plan, dryRun: true };
|
|
6953
|
-
if (!
|
|
7439
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
6954
7440
|
const db = getDb();
|
|
6955
7441
|
for (const entry of collectBundledCatalog()) {
|
|
6956
7442
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
@@ -6965,19 +7451,20 @@ async function syncHooks(options = {}) {
|
|
|
6965
7451
|
}
|
|
6966
7452
|
return plan;
|
|
6967
7453
|
}
|
|
6968
|
-
const {
|
|
7454
|
+
const { origin, apiKey } = transport.authority;
|
|
7455
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
6969
7456
|
const diff = computeDiff(catalog, remoteLock);
|
|
6970
|
-
const staged = await stageSyncArtifacts(
|
|
6971
|
-
await commitSyncArtifacts(staged,
|
|
6972
|
-
return { apiUrl, dryRun: false, diff };
|
|
7457
|
+
const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
|
|
7458
|
+
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
7459
|
+
return { apiUrl: origin, dryRun: false, diff };
|
|
6973
7460
|
}
|
|
6974
|
-
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
7461
|
+
async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
6975
7462
|
const staged = [];
|
|
6976
7463
|
for (const name of names) {
|
|
6977
7464
|
const entry = remoteLock.hooks[name];
|
|
6978
7465
|
if (!entry)
|
|
6979
7466
|
continue;
|
|
6980
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}
|
|
7467
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
|
|
6981
7468
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6982
7469
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6983
7470
|
}
|
|
@@ -7044,8 +7531,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7044
7531
|
throw err;
|
|
7045
7532
|
}
|
|
7046
7533
|
}
|
|
7047
|
-
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7048
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
7534
|
+
async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
7535
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7049
7536
|
const entry = remoteLock.hooks?.[name];
|
|
7050
7537
|
if (!entry) {
|
|
7051
7538
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
@@ -7054,7 +7541,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7054
7541
|
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7055
7542
|
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
7056
7543
|
}
|
|
7057
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
7544
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
|
|
7058
7545
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7059
7546
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7060
7547
|
}
|
|
@@ -7107,7 +7594,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7107
7594
|
}
|
|
7108
7595
|
var init_sync = __esm(() => {
|
|
7109
7596
|
init_registry();
|
|
7110
|
-
|
|
7597
|
+
init_transport2();
|
|
7111
7598
|
init_db();
|
|
7112
7599
|
init_store();
|
|
7113
7600
|
init_manifest();
|
|
@@ -7747,7 +8234,7 @@ var init_openapi = __esm(() => {
|
|
|
7747
8234
|
apiKey: {
|
|
7748
8235
|
type: "http",
|
|
7749
8236
|
scheme: "bearer",
|
|
7750
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
8237
|
+
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."
|
|
7751
8238
|
}
|
|
7752
8239
|
}
|
|
7753
8240
|
}
|
|
@@ -7764,13 +8251,13 @@ __export(exports_serve, {
|
|
|
7764
8251
|
SERVE_HOST: () => SERVE_HOST,
|
|
7765
8252
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
7766
8253
|
});
|
|
7767
|
-
import { readFileSync as
|
|
7768
|
-
import { dirname as dirname6, join as
|
|
8254
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8255
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
7769
8256
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7770
8257
|
function packageVersion() {
|
|
7771
8258
|
try {
|
|
7772
|
-
const pkgPath =
|
|
7773
|
-
return JSON.parse(
|
|
8259
|
+
const pkgPath = join15(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8260
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
|
|
7774
8261
|
} catch {
|
|
7775
8262
|
return "0.0.0";
|
|
7776
8263
|
}
|
|
@@ -7927,12 +8414,11 @@ function resolveServeOptions(options) {
|
|
|
7927
8414
|
}
|
|
7928
8415
|
function startServeServer(options) {
|
|
7929
8416
|
const { port, host } = resolveServeOptions(options);
|
|
7930
|
-
const apiKey = resolveApiKey();
|
|
7931
8417
|
const server = Bun.serve({
|
|
7932
8418
|
hostname: host,
|
|
7933
8419
|
port,
|
|
7934
8420
|
async fetch(req) {
|
|
7935
|
-
return handleServeRequest(req,
|
|
8421
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
7936
8422
|
}
|
|
7937
8423
|
});
|
|
7938
8424
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
@@ -7944,7 +8430,7 @@ var init_serve = __esm(() => {
|
|
|
7944
8430
|
init_manifest();
|
|
7945
8431
|
init_store();
|
|
7946
8432
|
init_resolve();
|
|
7947
|
-
|
|
8433
|
+
init_transport2();
|
|
7948
8434
|
init_openapi();
|
|
7949
8435
|
init_semver();
|
|
7950
8436
|
if (false) {}
|
|
@@ -8046,17 +8532,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
8046
8532
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8047
8533
|
import { z as z2 } from "zod";
|
|
8048
8534
|
import { createServer } from "http";
|
|
8049
|
-
import { existsSync as
|
|
8050
|
-
import { join as
|
|
8535
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
8536
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
8051
8537
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8052
8538
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
8053
8539
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
8054
8540
|
const resolved = resolveHook2(name);
|
|
8055
8541
|
const hookScript = resolveScriptPath2(name);
|
|
8056
|
-
if (!resolved || !hookScript || !
|
|
8542
|
+
if (!resolved || !hookScript || !existsSync14(hookScript)) {
|
|
8057
8543
|
throw new Error(`Hook '${name}' not found`);
|
|
8058
8544
|
}
|
|
8059
|
-
const content =
|
|
8545
|
+
const content = readFileSync10(hookScript);
|
|
8060
8546
|
const sha = sha256Of(content);
|
|
8061
8547
|
const check = checkScriptHash(name, sha);
|
|
8062
8548
|
if (!check.ok) {
|
|
@@ -8261,7 +8747,7 @@ function createHooksServer() {
|
|
|
8261
8747
|
const settingsPath = getSettingsPath(scope);
|
|
8262
8748
|
const issues = [];
|
|
8263
8749
|
const healthy = [];
|
|
8264
|
-
const settingsExist =
|
|
8750
|
+
const settingsExist = existsSync14(settingsPath);
|
|
8265
8751
|
if (!settingsExist) {
|
|
8266
8752
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
8267
8753
|
}
|
|
@@ -8271,7 +8757,7 @@ function createHooksServer() {
|
|
|
8271
8757
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
8272
8758
|
let hookHealthy = true;
|
|
8273
8759
|
if (custom) {
|
|
8274
|
-
if (!
|
|
8760
|
+
if (!existsSync14(custom.scriptPath)) {
|
|
8275
8761
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
8276
8762
|
hookHealthy = false;
|
|
8277
8763
|
}
|
|
@@ -8281,14 +8767,14 @@ function createHooksServer() {
|
|
|
8281
8767
|
continue;
|
|
8282
8768
|
}
|
|
8283
8769
|
const hookDir = getHookPath(name);
|
|
8284
|
-
if (!
|
|
8770
|
+
if (!existsSync14(join16(hookDir, "src", "hook.ts"))) {
|
|
8285
8771
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
8286
8772
|
hookHealthy = false;
|
|
8287
8773
|
}
|
|
8288
8774
|
}
|
|
8289
8775
|
if (meta && settingsExist) {
|
|
8290
8776
|
try {
|
|
8291
|
-
const settings = JSON.parse(
|
|
8777
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8292
8778
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
8293
8779
|
const eventName = meta.event.split(":")[0];
|
|
8294
8780
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -8318,10 +8804,10 @@ function createHooksServer() {
|
|
|
8318
8804
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
8319
8805
|
}
|
|
8320
8806
|
const hookPath = getHookPath(name);
|
|
8321
|
-
const readmePath =
|
|
8807
|
+
const readmePath = join16(hookPath, "README.md");
|
|
8322
8808
|
let readme = "";
|
|
8323
|
-
if (
|
|
8324
|
-
readme =
|
|
8809
|
+
if (existsSync14(readmePath)) {
|
|
8810
|
+
readme = readFileSync10(readmePath, "utf-8");
|
|
8325
8811
|
}
|
|
8326
8812
|
if (verbose) {
|
|
8327
8813
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -8504,7 +8990,7 @@ function createHooksServer() {
|
|
|
8504
8990
|
const ctx = {
|
|
8505
8991
|
scope,
|
|
8506
8992
|
settings_path: settingsPath,
|
|
8507
|
-
settings_exists:
|
|
8993
|
+
settings_exists: existsSync14(settingsPath),
|
|
8508
8994
|
registered_hooks: hooks,
|
|
8509
8995
|
hook_count: hooks.length,
|
|
8510
8996
|
healthy,
|
|
@@ -8650,8 +9136,8 @@ function createHooksServer() {
|
|
|
8650
9136
|
const settingsPath = getSettingsPath(scope);
|
|
8651
9137
|
let settings = {};
|
|
8652
9138
|
try {
|
|
8653
|
-
if (
|
|
8654
|
-
settings = JSON.parse(
|
|
9139
|
+
if (existsSync14(settingsPath))
|
|
9140
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8655
9141
|
} catch {}
|
|
8656
9142
|
if (!settings.hooks)
|
|
8657
9143
|
settings.hooks = {};
|
|
@@ -8659,10 +9145,10 @@ function createHooksServer() {
|
|
|
8659
9145
|
if (!disabled.includes(name))
|
|
8660
9146
|
disabled.push(name);
|
|
8661
9147
|
settings.hooks.__disabled = disabled;
|
|
8662
|
-
const { writeFileSync:
|
|
9148
|
+
const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
|
|
8663
9149
|
const { dirname: dirname8 } = await import("path");
|
|
8664
|
-
|
|
8665
|
-
|
|
9150
|
+
mkdirSync7(dirname8(settingsPath), { recursive: true });
|
|
9151
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
8666
9152
|
`);
|
|
8667
9153
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
8668
9154
|
});
|
|
@@ -8673,15 +9159,15 @@ function createHooksServer() {
|
|
|
8673
9159
|
const settingsPath = getSettingsPath(scope);
|
|
8674
9160
|
let settings = {};
|
|
8675
9161
|
try {
|
|
8676
|
-
if (
|
|
8677
|
-
settings = JSON.parse(
|
|
9162
|
+
if (existsSync14(settingsPath))
|
|
9163
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8678
9164
|
} catch {}
|
|
8679
9165
|
if (settings.hooks?.__disabled) {
|
|
8680
9166
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
8681
9167
|
if (settings.hooks.__disabled.length === 0)
|
|
8682
9168
|
delete settings.hooks.__disabled;
|
|
8683
|
-
const { writeFileSync:
|
|
8684
|
-
|
|
9169
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
9170
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
8685
9171
|
`);
|
|
8686
9172
|
}
|
|
8687
9173
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
@@ -9038,9 +9524,9 @@ var init_server = __esm(() => {
|
|
|
9038
9524
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
9039
9525
|
try {
|
|
9040
9526
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
9041
|
-
const p =
|
|
9042
|
-
if (
|
|
9043
|
-
pkg = JSON.parse(
|
|
9527
|
+
const p = join16(__dirname3, rel);
|
|
9528
|
+
if (existsSync14(p)) {
|
|
9529
|
+
pkg = JSON.parse(readFileSync10(p, "utf-8"));
|
|
9044
9530
|
break;
|
|
9045
9531
|
}
|
|
9046
9532
|
}
|
|
@@ -9112,12 +9598,12 @@ var init_http = () => {};
|
|
|
9112
9598
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
9113
9599
|
import { Buffer as Buffer2 } from "buffer";
|
|
9114
9600
|
import { existsSync as existsSync2 } from "fs";
|
|
9115
|
-
import { join as
|
|
9601
|
+
import { join as join2 } from "path";
|
|
9116
9602
|
import { existsSync } from "fs";
|
|
9117
|
-
import { homedir as homedir2 } from "os";
|
|
9118
|
-
import { join as join2, resolve } from "path";
|
|
9119
9603
|
import { homedir } from "os";
|
|
9120
|
-
import { join } from "path";
|
|
9604
|
+
import { join, resolve } from "path";
|
|
9605
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
9606
|
+
import { join as pathsResolverJoin } from "path";
|
|
9121
9607
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
9122
9608
|
import { lookup as dnsLookup } from "dns/promises";
|
|
9123
9609
|
import { isIP } from "net";
|
|
@@ -9221,75 +9707,72 @@ function channelMatchesEvent(channel, event) {
|
|
|
9221
9707
|
return true;
|
|
9222
9708
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
9223
9709
|
}
|
|
9224
|
-
var
|
|
9710
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
9225
9711
|
config: "HASNA_CONFIG_HOME",
|
|
9226
9712
|
data: "HASNA_DATA_HOME",
|
|
9227
9713
|
state: "HASNA_STATE_HOME",
|
|
9228
9714
|
cache: "HASNA_CACHE_HOME"
|
|
9229
9715
|
};
|
|
9230
|
-
var
|
|
9231
|
-
function
|
|
9716
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9717
|
+
function pathsResolverAssertApp(app) {
|
|
9232
9718
|
if (typeof app !== "string" || app.length === 0) {
|
|
9233
9719
|
throw new TypeError("paths: app must be a non-empty string");
|
|
9234
9720
|
}
|
|
9235
|
-
if (!
|
|
9721
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
9236
9722
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
9237
9723
|
}
|
|
9238
9724
|
}
|
|
9239
|
-
function
|
|
9240
|
-
|
|
9241
|
-
}
|
|
9242
|
-
|
|
9243
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
9244
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9245
|
-
}
|
|
9246
|
-
function isMacOS(platform) {
|
|
9247
|
-
return platform === "darwin";
|
|
9725
|
+
function pathsResolverAssertKind(kind) {
|
|
9726
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
9727
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
9728
|
+
}
|
|
9248
9729
|
}
|
|
9249
|
-
function
|
|
9250
|
-
|
|
9251
|
-
|
|
9730
|
+
function pathsResolverBaseDir(kind, options) {
|
|
9731
|
+
pathsResolverAssertKind(kind);
|
|
9732
|
+
const env = options.env ?? process.env;
|
|
9733
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
9734
|
+
if (typeof override === "string" && override.length > 0)
|
|
9252
9735
|
return override;
|
|
9253
|
-
const home = options.home ??
|
|
9736
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
9254
9737
|
const platform = options.platform ?? process.platform;
|
|
9255
|
-
if (
|
|
9738
|
+
if (platform === "darwin") {
|
|
9256
9739
|
switch (kind) {
|
|
9257
9740
|
case "config":
|
|
9258
9741
|
case "data":
|
|
9259
|
-
return
|
|
9742
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
9260
9743
|
case "cache":
|
|
9261
|
-
return
|
|
9744
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
9262
9745
|
case "state":
|
|
9263
|
-
return
|
|
9746
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
9264
9747
|
}
|
|
9265
9748
|
}
|
|
9266
9749
|
switch (kind) {
|
|
9267
9750
|
case "config":
|
|
9268
|
-
return
|
|
9751
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
9269
9752
|
case "data":
|
|
9270
|
-
return
|
|
9753
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
9271
9754
|
case "state":
|
|
9272
|
-
return
|
|
9755
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
9273
9756
|
case "cache":
|
|
9274
|
-
return
|
|
9757
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
9275
9758
|
}
|
|
9276
9759
|
}
|
|
9277
|
-
function
|
|
9278
|
-
|
|
9279
|
-
const appSegment = options.internal === true ?
|
|
9280
|
-
return
|
|
9760
|
+
function pathsResolverResolve(kind, options) {
|
|
9761
|
+
pathsResolverAssertApp(options.app);
|
|
9762
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
9763
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
9281
9764
|
}
|
|
9282
9765
|
function dataDir(options) {
|
|
9283
|
-
return
|
|
9766
|
+
return pathsResolverResolve("data", options);
|
|
9284
9767
|
}
|
|
9285
9768
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
9286
9769
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
9287
9770
|
var EVENTS_STORE_SENTINEL_FILE = "events.json";
|
|
9288
9771
|
function effectiveHome() {
|
|
9289
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
9772
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
9290
9773
|
}
|
|
9291
9774
|
function legacyHomeDir() {
|
|
9292
|
-
return
|
|
9775
|
+
return join(effectiveHome(), ".hasna", "events");
|
|
9293
9776
|
}
|
|
9294
9777
|
function resolverHome() {
|
|
9295
9778
|
return dataDir({ app: "events", home: effectiveHome() || undefined });
|
|
@@ -9298,7 +9781,7 @@ function adoptResolverHome(resolved, env = process.env) {
|
|
|
9298
9781
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
9299
9782
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
9300
9783
|
return true;
|
|
9301
|
-
return existsSync(
|
|
9784
|
+
return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
9302
9785
|
}
|
|
9303
9786
|
function exactEventsHome() {
|
|
9304
9787
|
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
@@ -9339,9 +9822,9 @@ class JsonEventsStore {
|
|
|
9339
9822
|
constructor(dataDir2 = getEventsDataDir()) {
|
|
9340
9823
|
this.dataDir = dataDir2;
|
|
9341
9824
|
this.runtime = localJsonRuntime(dataDir2);
|
|
9342
|
-
this.channelsPath =
|
|
9343
|
-
this.eventsPath =
|
|
9344
|
-
this.deliveriesPath =
|
|
9825
|
+
this.channelsPath = join2(dataDir2, "channels.json");
|
|
9826
|
+
this.eventsPath = join2(dataDir2, "events.json");
|
|
9827
|
+
this.deliveriesPath = join2(dataDir2, "deliveries.json");
|
|
9345
9828
|
}
|
|
9346
9829
|
async init() {
|
|
9347
9830
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -9609,7 +10092,7 @@ async function getEventsStatus(dataDir2) {
|
|
|
9609
10092
|
};
|
|
9610
10093
|
}
|
|
9611
10094
|
function statusFile(dataDir2, fileName, records) {
|
|
9612
|
-
const path =
|
|
10095
|
+
const path = join2(dataDir2, fileName);
|
|
9613
10096
|
return { path, exists: existsSync2(path), records };
|
|
9614
10097
|
}
|
|
9615
10098
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
@@ -10543,6 +11026,13 @@ function parseMatcherExpression(value, label) {
|
|
|
10543
11026
|
negated: false
|
|
10544
11027
|
};
|
|
10545
11028
|
}
|
|
11029
|
+
function webhookTargetPolicyFromEnv() {
|
|
11030
|
+
const value = process.env.HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS;
|
|
11031
|
+
if (!value)
|
|
11032
|
+
return;
|
|
11033
|
+
const hosts = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
11034
|
+
return hosts.length > 0 ? { allowPrivateHosts: hosts } : undefined;
|
|
11035
|
+
}
|
|
10546
11036
|
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
10547
11037
|
function parseJsonObject(value, fallback) {
|
|
10548
11038
|
if (!value)
|
|
@@ -10568,7 +11058,7 @@ function parseHeaders(values) {
|
|
|
10568
11058
|
function createClient(options) {
|
|
10569
11059
|
if (options.createClient)
|
|
10570
11060
|
return options.createClient();
|
|
10571
|
-
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
11061
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir), webhookTargetPolicy: webhookTargetPolicyFromEnv() });
|
|
10572
11062
|
}
|
|
10573
11063
|
function print(value, json, text) {
|
|
10574
11064
|
if (json)
|
|
@@ -10649,6 +11139,8 @@ function registerChannelCommands(program, options) {
|
|
|
10649
11139
|
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
10650
11140
|
}, { honorFilters: actionOptions.honorFilters });
|
|
10651
11141
|
print(result, json, `${result.status}: ${result.channelId}`);
|
|
11142
|
+
if (result.status === "failed")
|
|
11143
|
+
process.exitCode = 1;
|
|
10652
11144
|
} catch (error) {
|
|
10653
11145
|
fail(error, json);
|
|
10654
11146
|
}
|
|
@@ -10759,8 +11251,8 @@ var {
|
|
|
10759
11251
|
|
|
10760
11252
|
// src/cli/index.tsx
|
|
10761
11253
|
import chalk2 from "chalk";
|
|
10762
|
-
import { existsSync as
|
|
10763
|
-
import { join as
|
|
11254
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
|
|
11255
|
+
import { join as join17, dirname as dirname8 } from "path";
|
|
10764
11256
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
10765
11257
|
|
|
10766
11258
|
// src/cli/components/App.tsx
|
|
@@ -11943,10 +12435,12 @@ init_store();
|
|
|
11943
12435
|
init_semver();
|
|
11944
12436
|
init_app_home();
|
|
11945
12437
|
init_config();
|
|
12438
|
+
init_local_opt_in();
|
|
12439
|
+
init_transport2();
|
|
11946
12440
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
11947
12441
|
var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
|
|
11948
|
-
var pkgPath =
|
|
11949
|
-
var pkg2 = JSON.parse(
|
|
12442
|
+
var pkgPath = existsSync15(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
|
|
12443
|
+
var pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
11950
12444
|
var program2 = new Command;
|
|
11951
12445
|
function resolveScope(options) {
|
|
11952
12446
|
if (options.project)
|
|
@@ -11980,7 +12474,7 @@ function projectLogRows2(rows) {
|
|
|
11980
12474
|
function readToken(tokenFile) {
|
|
11981
12475
|
if (tokenFile) {
|
|
11982
12476
|
try {
|
|
11983
|
-
return
|
|
12477
|
+
return readFileSync11(tokenFile, "utf-8").trim();
|
|
11984
12478
|
} catch {
|
|
11985
12479
|
return;
|
|
11986
12480
|
}
|
|
@@ -12045,7 +12539,6 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
|
|
|
12045
12539
|
});
|
|
12046
12540
|
program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
|
|
12047
12541
|
if (options.cloudflare) {
|
|
12048
|
-
const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
12049
12542
|
if (!options.apiUrl) {
|
|
12050
12543
|
const message = "--cloudflare requires --api-url <url>";
|
|
12051
12544
|
if (options.json)
|
|
@@ -12055,22 +12548,39 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
12055
12548
|
return;
|
|
12056
12549
|
}
|
|
12057
12550
|
const apiKeyRef = options.apiKey ?? "hasna/hooks/live/api-key";
|
|
12058
|
-
const
|
|
12059
|
-
const path = writeConfig2(config);
|
|
12551
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
12060
12552
|
if (options.json) {
|
|
12061
|
-
console.log(JSON.stringify({
|
|
12553
|
+
console.log(JSON.stringify({
|
|
12554
|
+
ok: true,
|
|
12555
|
+
api_url: apiUrl,
|
|
12556
|
+
api_key_ref: apiKeyRef,
|
|
12557
|
+
env: {
|
|
12558
|
+
HASNA_HOOKS_API_URL: apiUrl,
|
|
12559
|
+
HASNA_HOOKS_API_KEY_REF: apiKeyRef
|
|
12560
|
+
},
|
|
12561
|
+
note: "config.json is retired; no file was written"
|
|
12562
|
+
}));
|
|
12062
12563
|
return;
|
|
12063
12564
|
}
|
|
12064
12565
|
console.log(chalk2.green(`
|
|
12065
|
-
\u2713 Remote registry
|
|
12566
|
+
\u2713 Remote registry configuration for ${apiUrl}
|
|
12066
12567
|
`));
|
|
12067
|
-
console.log(` ${chalk2.dim("API URL:")} ${
|
|
12068
|
-
console.log(` ${chalk2.dim("API key ref:")} ${
|
|
12069
|
-
console.log(
|
|
12568
|
+
console.log(` ${chalk2.dim("API URL:")} ${apiUrl}`);
|
|
12569
|
+
console.log(` ${chalk2.dim("API key ref:")} ${apiKeyRef}`);
|
|
12570
|
+
console.log();
|
|
12571
|
+
console.log(chalk2.dim(" ~/.hasna/hooks/config.json is retired (hasna/apps#1720); the registry"));
|
|
12572
|
+
console.log(chalk2.dim(" URL and key now resolve through @hasna/contracts, per call, in this order:"));
|
|
12573
|
+
console.log(chalk2.dim(" HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY (env)"));
|
|
12574
|
+
console.log(chalk2.dim(" hasna.credentials.hooks.api-url / .api-key (macOS Keychain)"));
|
|
12575
|
+
console.log(chalk2.dim(" ~/.hasna/hooks/config/credentials (disk, owner-only)"));
|
|
12576
|
+
console.log();
|
|
12577
|
+
console.log(chalk2.dim(" Configure the machine for this registry:"));
|
|
12578
|
+
console.log(` export HASNA_HOOKS_API_URL=${apiUrl}`);
|
|
12579
|
+
console.log(` export HASNA_HOOKS_API_KEY_REF=${apiKeyRef} # resolved through the vault, never a value`);
|
|
12070
12580
|
console.log();
|
|
12071
|
-
console.log(chalk2.dim("
|
|
12072
|
-
console.log(
|
|
12073
|
-
console.log(
|
|
12581
|
+
console.log(chalk2.dim(" Run the server with the key resolved from the vault, never the value:"));
|
|
12582
|
+
console.log(` secrets exec ${apiKeyRef} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
12583
|
+
console.log(chalk2.dim(" (or store the Keychain item hasna.credentials.hooks.api-key)"));
|
|
12074
12584
|
console.log();
|
|
12075
12585
|
return;
|
|
12076
12586
|
}
|
|
@@ -12097,7 +12607,7 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
12097
12607
|
if (profile.name) {
|
|
12098
12608
|
console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
|
|
12099
12609
|
}
|
|
12100
|
-
console.log(` ${chalk2.dim("Profile:")} ${
|
|
12610
|
+
console.log(` ${chalk2.dim("Profile:")} ${join17(getProfilesDir(), `${profile.agent_id}.json`)}`);
|
|
12101
12611
|
console.log();
|
|
12102
12612
|
console.log(chalk2.dim(" Install hooks with this profile:"));
|
|
12103
12613
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
@@ -12113,11 +12623,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
12113
12623
|
process.exit(1);
|
|
12114
12624
|
}
|
|
12115
12625
|
const hookScript = resolveScriptPath2(hook);
|
|
12116
|
-
if (!hookScript || !
|
|
12626
|
+
if (!hookScript || !existsSync15(hookScript)) {
|
|
12117
12627
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
12118
12628
|
process.exit(1);
|
|
12119
12629
|
}
|
|
12120
|
-
const content =
|
|
12630
|
+
const content = readFileSync11(hookScript);
|
|
12121
12631
|
const sha = sha256Of2(content);
|
|
12122
12632
|
const check = checkScriptHash2(hook, sha);
|
|
12123
12633
|
if (!check.ok) {
|
|
@@ -12258,7 +12768,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
12258
12768
|
}
|
|
12259
12769
|
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
12260
12770
|
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12261
|
-
const {
|
|
12771
|
+
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12262
12772
|
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12263
12773
|
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
12264
12774
|
function parseNameVersion(arg) {
|
|
@@ -12327,7 +12837,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
12327
12837
|
const { customHookDir: customHookDir2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12328
12838
|
const { removeHookFromStore: removeHookFromStore2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12329
12839
|
try {
|
|
12330
|
-
|
|
12840
|
+
rmSync4(customHookDir2(custom.name), { recursive: true, force: true });
|
|
12331
12841
|
} catch {}
|
|
12332
12842
|
removeHookFromStore2(custom.name);
|
|
12333
12843
|
throw pinError;
|
|
@@ -12351,11 +12861,13 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
12351
12861
|
const installedPinned = [];
|
|
12352
12862
|
for (const { arg, pinned } of pinnedRequests) {
|
|
12353
12863
|
try {
|
|
12354
|
-
const
|
|
12355
|
-
if (!
|
|
12356
|
-
throw new Error(`Cannot install '${arg}': no remote registry configured (set
|
|
12864
|
+
const transport = resolveHooksTransport2();
|
|
12865
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
12866
|
+
throw new Error(`Cannot install '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
12867
|
+
}
|
|
12868
|
+
const { origin, apiKey } = transport.authority;
|
|
12357
12869
|
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12358
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version,
|
|
12870
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
12359
12871
|
installedPinned.push(pinnedInstall.name);
|
|
12360
12872
|
if (options.json) {
|
|
12361
12873
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
@@ -12685,7 +13197,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12685
13197
|
const settingsPath = getSettingsPath(scope);
|
|
12686
13198
|
const issues = [];
|
|
12687
13199
|
const healthy = [];
|
|
12688
|
-
const settingsExist =
|
|
13200
|
+
const settingsExist = existsSync15(settingsPath);
|
|
12689
13201
|
if (!settingsExist) {
|
|
12690
13202
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
12691
13203
|
}
|
|
@@ -12693,7 +13205,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12693
13205
|
let wiringCount = 0;
|
|
12694
13206
|
if (settingsExist) {
|
|
12695
13207
|
try {
|
|
12696
|
-
const settings = JSON.parse(
|
|
13208
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
12697
13209
|
wiringCount = countSettingsWiring(settings);
|
|
12698
13210
|
} catch {}
|
|
12699
13211
|
}
|
|
@@ -12702,7 +13214,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12702
13214
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
12703
13215
|
let hookHealthy = true;
|
|
12704
13216
|
if (custom) {
|
|
12705
|
-
if (!
|
|
13217
|
+
if (!existsSync15(custom.scriptPath)) {
|
|
12706
13218
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
12707
13219
|
hookHealthy = false;
|
|
12708
13220
|
}
|
|
@@ -12713,15 +13225,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12713
13225
|
continue;
|
|
12714
13226
|
}
|
|
12715
13227
|
const hookDir = getHookPath(name);
|
|
12716
|
-
const hookScript =
|
|
12717
|
-
if (!
|
|
13228
|
+
const hookScript = join17(hookDir, "src", "hook.ts");
|
|
13229
|
+
if (!existsSync15(hookScript)) {
|
|
12718
13230
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
12719
13231
|
hookHealthy = false;
|
|
12720
13232
|
}
|
|
12721
13233
|
}
|
|
12722
13234
|
if (meta && settingsExist) {
|
|
12723
13235
|
try {
|
|
12724
|
-
const settings = JSON.parse(
|
|
13236
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
12725
13237
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
12726
13238
|
const eventName = meta.event.split(":")[0];
|
|
12727
13239
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -12788,7 +13300,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
12788
13300
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
12789
13301
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12790
13302
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12791
|
-
const {
|
|
13303
|
+
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12792
13304
|
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
12793
13305
|
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12794
13306
|
function parseNameVersion(arg) {
|
|
@@ -12810,11 +13322,13 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
12810
13322
|
const pinned = parseNameVersion(arg);
|
|
12811
13323
|
if (pinned) {
|
|
12812
13324
|
try {
|
|
12813
|
-
const
|
|
12814
|
-
if (!
|
|
12815
|
-
throw new Error(`Cannot update '${arg}': no remote registry configured (set
|
|
13325
|
+
const transport = resolveHooksTransport2();
|
|
13326
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
13327
|
+
throw new Error(`Cannot update '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13328
|
+
}
|
|
13329
|
+
const { origin, apiKey } = transport.authority;
|
|
12816
13330
|
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12817
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version,
|
|
13331
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
12818
13332
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
12819
13333
|
if (!registered.success) {
|
|
12820
13334
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -12888,10 +13402,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
12888
13402
|
return;
|
|
12889
13403
|
}
|
|
12890
13404
|
const hookPath = getHookPath(hook);
|
|
12891
|
-
const readmePath =
|
|
13405
|
+
const readmePath = join17(hookPath, "README.md");
|
|
12892
13406
|
let readme = "";
|
|
12893
|
-
if (
|
|
12894
|
-
readme =
|
|
13407
|
+
if (existsSync15(readmePath)) {
|
|
13408
|
+
readme = readFileSync11(readmePath, "utf-8");
|
|
12895
13409
|
}
|
|
12896
13410
|
if (options.json) {
|
|
12897
13411
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -13075,8 +13589,8 @@ program2.command("profile-export").description("Export all agent profiles as JSO
|
|
|
13075
13589
|
const profiles = exportProfiles();
|
|
13076
13590
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
13077
13591
|
if (options.output) {
|
|
13078
|
-
const { writeFileSync:
|
|
13079
|
-
|
|
13592
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
13593
|
+
writeFileSync6(options.output, json2 + `
|
|
13080
13594
|
`);
|
|
13081
13595
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
13082
13596
|
} else {
|
|
@@ -13088,9 +13602,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
13088
13602
|
if (file === "-") {
|
|
13089
13603
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
13090
13604
|
} else {
|
|
13091
|
-
const { readFileSync:
|
|
13605
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
13092
13606
|
try {
|
|
13093
|
-
raw =
|
|
13607
|
+
raw = readFileSync12(file, "utf-8");
|
|
13094
13608
|
} catch {
|
|
13095
13609
|
if (options.json) {
|
|
13096
13610
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -13370,14 +13884,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
13370
13884
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
13371
13885
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
13372
13886
|
});
|
|
13373
|
-
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from
|
|
13887
|
+
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from the @hasna/contracts chain per request (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials) \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
|
|
13374
13888
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
13375
13889
|
startServeServer2({
|
|
13376
13890
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
13377
13891
|
host: options.host
|
|
13378
13892
|
});
|
|
13379
13893
|
});
|
|
13380
|
-
program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no
|
|
13894
|
+
program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no registry credential resolves and local mode is opted into)").action(async (options) => {
|
|
13381
13895
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13382
13896
|
try {
|
|
13383
13897
|
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
@@ -13508,4 +14022,62 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
13508
14022
|
}
|
|
13509
14023
|
});
|
|
13510
14024
|
registerEventsCommands(program2, { source: "hooks" });
|
|
14025
|
+
var API_INDEPENDENT_COMMANDS = new Set([
|
|
14026
|
+
"run",
|
|
14027
|
+
"serve",
|
|
14028
|
+
"mcp",
|
|
14029
|
+
"cf",
|
|
14030
|
+
"migrate",
|
|
14031
|
+
"init",
|
|
14032
|
+
"profile-export",
|
|
14033
|
+
"profile-import",
|
|
14034
|
+
"channels",
|
|
14035
|
+
"events"
|
|
14036
|
+
]);
|
|
14037
|
+
function failClosedForMissingApiEnv(detail) {
|
|
14038
|
+
console.error(chalk2.red(`hooks: no registry credential resolved and local mode is not explicitly enabled.
|
|
14039
|
+
` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
|
|
14040
|
+
` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
|
|
14041
|
+
` + `config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
|
|
14042
|
+
` + `HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
|
|
14043
|
+
` + (detail ? `${detail}
|
|
14044
|
+
` : "") + "Refusing to silently fall back to local storage."));
|
|
14045
|
+
process.exit(1);
|
|
14046
|
+
}
|
|
14047
|
+
function firstCommandToken(argv) {
|
|
14048
|
+
for (const arg of argv) {
|
|
14049
|
+
if (arg === "--")
|
|
14050
|
+
continue;
|
|
14051
|
+
if (arg.startsWith("-"))
|
|
14052
|
+
continue;
|
|
14053
|
+
return arg;
|
|
14054
|
+
}
|
|
14055
|
+
return null;
|
|
14056
|
+
}
|
|
14057
|
+
function enforceTransportGate() {
|
|
14058
|
+
const argv = process.argv.slice(2);
|
|
14059
|
+
if (argv.some((arg) => arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version"))
|
|
14060
|
+
return;
|
|
14061
|
+
const token = firstCommandToken(argv);
|
|
14062
|
+
if (token === "help")
|
|
14063
|
+
return;
|
|
14064
|
+
if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
|
|
14065
|
+
return;
|
|
14066
|
+
if (hasHooksEnvAuthorityIntent(process.env))
|
|
14067
|
+
return;
|
|
14068
|
+
if (isHooksLocalOptIn(process.env)) {
|
|
14069
|
+
announceHooksLocalMode();
|
|
14070
|
+
return;
|
|
14071
|
+
}
|
|
14072
|
+
try {
|
|
14073
|
+
const transport = resolveHooksTransport(process.env);
|
|
14074
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
14075
|
+
failClosedForMissingApiEnv();
|
|
14076
|
+
}
|
|
14077
|
+
} catch (error) {
|
|
14078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14079
|
+
failClosedForMissingApiEnv(message);
|
|
14080
|
+
}
|
|
14081
|
+
}
|
|
14082
|
+
enforceTransportGate();
|
|
13511
14083
|
program2.parse();
|