@hasna/hooks 0.7.9 → 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 +1573 -695
- package/bin/serve.js +713 -335
- package/dist/config.d.ts +25 -19
- package/dist/db/index.d.ts +3 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1162 -473
- package/dist/lib/app-home.d.ts +74 -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 +14 -1
- 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/serve.d.ts +7 -0
- package/dist/storage.js +141 -27
- 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 +6 -9
- package/scripts/artifact-scan.ts +49 -0
- package/scripts/ensure-profiles-dir.mjs +99 -0
- package/scripts/validate-package.ts +280 -0
package/bin/index.js
CHANGED
|
@@ -1886,6 +1886,123 @@ var require_commander = __commonJS((exports) => {
|
|
|
1886
1886
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
1887
1887
|
});
|
|
1888
1888
|
|
|
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) {
|
|
1896
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
1897
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
1898
|
+
}
|
|
1899
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
1900
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
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
|
+
}
|
|
1907
|
+
}
|
|
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
|
+
return override;
|
|
1914
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
1915
|
+
const platform = options.platform ?? process.platform;
|
|
1916
|
+
if (platform === "darwin") {
|
|
1917
|
+
switch (kind) {
|
|
1918
|
+
case "config":
|
|
1919
|
+
case "data":
|
|
1920
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
1921
|
+
case "cache":
|
|
1922
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
1923
|
+
case "state":
|
|
1924
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
switch (kind) {
|
|
1928
|
+
case "config":
|
|
1929
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
1930
|
+
case "data":
|
|
1931
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
1932
|
+
case "state":
|
|
1933
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
1934
|
+
case "cache":
|
|
1935
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
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
|
+
}
|
|
1943
|
+
function dataDir2(options) {
|
|
1944
|
+
return pathsResolverResolve2("data", options);
|
|
1945
|
+
}
|
|
1946
|
+
function getHomeDir(env = process.env) {
|
|
1947
|
+
return env.HOME || env.USERPROFILE || homedir2();
|
|
1948
|
+
}
|
|
1949
|
+
function getResolverDataRoot(env = process.env) {
|
|
1950
|
+
return dataDir2({ app: "hooks", home: getHomeDir(env), env });
|
|
1951
|
+
}
|
|
1952
|
+
function getLegacyDataRoot(env = process.env) {
|
|
1953
|
+
return join3(getHomeDir(env), ".hasna", "hooks");
|
|
1954
|
+
}
|
|
1955
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
1956
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
1957
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
1958
|
+
return true;
|
|
1959
|
+
return existsSync3(join3(resolved, STORE_MARKER));
|
|
1960
|
+
}
|
|
1961
|
+
function getExplicitDataDir(env = process.env) {
|
|
1962
|
+
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
1963
|
+
if (typeof dir === "string" && dir.trim().length > 0)
|
|
1964
|
+
return dir.trim();
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
function getExactDataRoot(env = process.env) {
|
|
1968
|
+
const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
|
|
1969
|
+
if (dir)
|
|
1970
|
+
return resolve2(dir);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
function getEffectiveDataRoot(env = process.env) {
|
|
1974
|
+
const explicit = getExplicitDataDir(env);
|
|
1975
|
+
if (explicit)
|
|
1976
|
+
return explicit;
|
|
1977
|
+
const exact = getExactDataRoot(env);
|
|
1978
|
+
if (exact)
|
|
1979
|
+
return exact;
|
|
1980
|
+
const resolved = getResolverDataRoot(env);
|
|
1981
|
+
return adoptResolverDataRoot(resolved, env) ? resolve2(resolved) : getLegacyDataRoot(env);
|
|
1982
|
+
}
|
|
1983
|
+
function getReportedDbPath(env = process.env) {
|
|
1984
|
+
const explicit = getExplicitDbPath(env);
|
|
1985
|
+
if (explicit)
|
|
1986
|
+
return explicit;
|
|
1987
|
+
return join3(getEffectiveDataRoot(env), "hooks.db");
|
|
1988
|
+
}
|
|
1989
|
+
function getExplicitDbPath(env = process.env) {
|
|
1990
|
+
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
1991
|
+
if (typeof db === "string" && db.trim().length > 0)
|
|
1992
|
+
return db.trim();
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
var PATHS_RESOLVER_KIND_ENV2, PATHS_RESOLVER_APP_SLUG_RE2, STORE_MARKER = "hooks.db";
|
|
1996
|
+
var init_app_home = __esm(() => {
|
|
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]+)*$/;
|
|
2004
|
+
});
|
|
2005
|
+
|
|
1889
2006
|
// src/lib/registry.ts
|
|
1890
2007
|
function getHooksByCategory(category) {
|
|
1891
2008
|
return HOOKS.filter((h) => h.category === category);
|
|
@@ -1902,6 +2019,7 @@ function getHook(name) {
|
|
|
1902
2019
|
}
|
|
1903
2020
|
var CATEGORIES, HOOKS;
|
|
1904
2021
|
var init_registry = __esm(() => {
|
|
2022
|
+
init_app_home();
|
|
1905
2023
|
CATEGORIES = [
|
|
1906
2024
|
"Git Safety",
|
|
1907
2025
|
"Code Quality",
|
|
@@ -2229,7 +2347,7 @@ var init_registry = __esm(() => {
|
|
|
2229
2347
|
{
|
|
2230
2348
|
name: "sessionlog",
|
|
2231
2349
|
displayName: "Session Log",
|
|
2232
|
-
description:
|
|
2350
|
+
description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
|
|
2233
2351
|
version: "0.1.0",
|
|
2234
2352
|
category: "Observability",
|
|
2235
2353
|
event: "PostToolUse",
|
|
@@ -2239,7 +2357,7 @@ var init_registry = __esm(() => {
|
|
|
2239
2357
|
{
|
|
2240
2358
|
name: "commandlog",
|
|
2241
2359
|
displayName: "Command Log",
|
|
2242
|
-
description:
|
|
2360
|
+
description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
|
|
2243
2361
|
version: "0.1.0",
|
|
2244
2362
|
category: "Observability",
|
|
2245
2363
|
event: "PostToolUse",
|
|
@@ -2259,7 +2377,7 @@ var init_registry = __esm(() => {
|
|
|
2259
2377
|
{
|
|
2260
2378
|
name: "errornotify",
|
|
2261
2379
|
displayName: "Error Notify",
|
|
2262
|
-
description:
|
|
2380
|
+
description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
|
|
2263
2381
|
version: "0.1.0",
|
|
2264
2382
|
category: "Observability",
|
|
2265
2383
|
event: "PostToolUse",
|
|
@@ -4069,345 +4187,22 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4069
4187
|
module.exports = spinners;
|
|
4070
4188
|
});
|
|
4071
4189
|
|
|
4072
|
-
// ../contracts/dist/client/transport.js
|
|
4073
|
-
import { readFileSync, statSync } from "fs";
|
|
4074
|
-
import { join as join2 } from "path";
|
|
4075
|
-
function envToken(name) {
|
|
4076
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
4077
|
-
}
|
|
4078
|
-
function clientTransportEnvKeys(name) {
|
|
4079
|
-
const envSegment = envToken(name);
|
|
4080
|
-
return {
|
|
4081
|
-
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4082
|
-
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4083
|
-
};
|
|
4084
|
-
}
|
|
4085
|
-
function credentialOverrideEnvKey(name) {
|
|
4086
|
-
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
4087
|
-
}
|
|
4088
|
-
function homeDir(env) {
|
|
4089
|
-
const home = env.HOME?.trim();
|
|
4090
|
-
return home ? home : null;
|
|
4091
|
-
}
|
|
4092
|
-
function credentialDiskSources(name, env) {
|
|
4093
|
-
return profileDiskSources(name, env, null);
|
|
4094
|
-
}
|
|
4095
|
-
function profileDiskSources(name, env, profile) {
|
|
4096
|
-
const home = homeDir(env);
|
|
4097
|
-
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4098
|
-
return [];
|
|
4099
|
-
const stem = profile ? `${name}.${profile}` : name;
|
|
4100
|
-
const configStem = profile ? `${name}-${profile}` : name;
|
|
4101
|
-
return [
|
|
4102
|
-
join2(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4103
|
-
join2(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
|
|
4104
|
-
];
|
|
4105
|
-
}
|
|
4106
|
-
function parseEnvFile(text) {
|
|
4107
|
-
const values = new Map;
|
|
4108
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
4109
|
-
const line = rawLine.trim();
|
|
4110
|
-
if (line.length === 0 || line.startsWith("#"))
|
|
4111
|
-
continue;
|
|
4112
|
-
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
4113
|
-
const equals = withoutExport.indexOf("=");
|
|
4114
|
-
if (equals <= 0)
|
|
4115
|
-
continue;
|
|
4116
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
4117
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4118
|
-
continue;
|
|
4119
|
-
let value = withoutExport.slice(equals + 1).trim();
|
|
4120
|
-
const quote = value[0];
|
|
4121
|
-
if (quote === '"' || quote === "'") {
|
|
4122
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
4123
|
-
continue;
|
|
4124
|
-
value = value.slice(1, -1);
|
|
4125
|
-
}
|
|
4126
|
-
if (value.length === 0)
|
|
4127
|
-
continue;
|
|
4128
|
-
values.set(key, value);
|
|
4129
|
-
}
|
|
4130
|
-
return values;
|
|
4131
|
-
}
|
|
4132
|
-
function readAppConfigFile(path) {
|
|
4133
|
-
let text;
|
|
4134
|
-
try {
|
|
4135
|
-
const stats = statSync(path);
|
|
4136
|
-
if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
4137
|
-
return null;
|
|
4138
|
-
text = readFileSync(path, "utf8");
|
|
4139
|
-
} catch {
|
|
4140
|
-
return null;
|
|
4141
|
-
}
|
|
4142
|
-
return parseEnvFile(text);
|
|
4143
|
-
}
|
|
4144
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
4145
|
-
const values = readAppConfigFile(path);
|
|
4146
|
-
if (!values)
|
|
4147
|
-
return null;
|
|
4148
|
-
for (const key of apiKeyKeys) {
|
|
4149
|
-
const value = values.get(key)?.trim();
|
|
4150
|
-
if (value)
|
|
4151
|
-
return value;
|
|
4152
|
-
}
|
|
4153
|
-
return null;
|
|
4154
|
-
}
|
|
4155
|
-
function assertUsableCredential(appName, source, value) {
|
|
4156
|
-
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
4157
|
-
return;
|
|
4158
|
-
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]);
|
|
4159
|
-
}
|
|
4160
|
-
function sealCredential(fields) {
|
|
4161
|
-
const { apiKey } = fields;
|
|
4162
|
-
const visible = {
|
|
4163
|
-
tier: fields.tier,
|
|
4164
|
-
source: fields.source,
|
|
4165
|
-
deliberate: fields.deliberate,
|
|
4166
|
-
deprecated: fields.deprecated,
|
|
4167
|
-
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
4168
|
-
warning: fields.warning
|
|
4169
|
-
};
|
|
4170
|
-
const sealed = { ...visible };
|
|
4171
|
-
Object.defineProperty(sealed, "apiKey", {
|
|
4172
|
-
value: apiKey,
|
|
4173
|
-
enumerable: false,
|
|
4174
|
-
writable: false,
|
|
4175
|
-
configurable: false
|
|
4176
|
-
});
|
|
4177
|
-
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4178
|
-
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4179
|
-
enumerable: false,
|
|
4180
|
-
writable: false,
|
|
4181
|
-
configurable: false
|
|
4182
|
-
});
|
|
4183
|
-
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4184
|
-
value: true,
|
|
4185
|
-
enumerable: false,
|
|
4186
|
-
writable: false,
|
|
4187
|
-
configurable: false
|
|
4188
|
-
});
|
|
4189
|
-
return Object.freeze(sealed);
|
|
4190
|
-
}
|
|
4191
|
-
function firstEnvValue(env, keys) {
|
|
4192
|
-
for (const key of keys) {
|
|
4193
|
-
const value = env[key]?.trim();
|
|
4194
|
-
if (value)
|
|
4195
|
-
return { key, value };
|
|
4196
|
-
}
|
|
4197
|
-
return null;
|
|
4198
|
-
}
|
|
4199
|
-
function deprecationNotified() {
|
|
4200
|
-
const host = globalThis;
|
|
4201
|
-
const existing = host[DEPRECATION_REGISTRY];
|
|
4202
|
-
if (existing instanceof Set)
|
|
4203
|
-
return existing;
|
|
4204
|
-
const created = new Set;
|
|
4205
|
-
host[DEPRECATION_REGISTRY] = created;
|
|
4206
|
-
return created;
|
|
4207
|
-
}
|
|
4208
|
-
function defaultDeprecationSink(message) {
|
|
4209
|
-
if (typeof process !== "undefined" && process.stderr) {
|
|
4210
|
-
process.stderr.write(`${message}
|
|
4211
|
-
`);
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
function resolveCredential(name, env, options = {}) {
|
|
4215
|
-
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
4216
|
-
const diskPaths = credentialDiskSources(name, env);
|
|
4217
|
-
const explicitKey = options.apiKey?.trim();
|
|
4218
|
-
if (explicitKey) {
|
|
4219
|
-
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4220
|
-
return sealCredential({
|
|
4221
|
-
apiKey: explicitKey,
|
|
4222
|
-
tier: "argument",
|
|
4223
|
-
source: "explicit apiKey argument",
|
|
4224
|
-
deliberate: true,
|
|
4225
|
-
deprecated: false,
|
|
4226
|
-
diskCandidates: diskPaths,
|
|
4227
|
-
warning: null
|
|
4228
|
-
});
|
|
4229
|
-
}
|
|
4230
|
-
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4231
|
-
const overrideRaw = env[overrideKeyName];
|
|
4232
|
-
if (overrideRaw !== undefined) {
|
|
4233
|
-
const override = overrideRaw.trim();
|
|
4234
|
-
if (!override) {
|
|
4235
|
-
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]);
|
|
4236
|
-
}
|
|
4237
|
-
assertUsableCredential(name, overrideKeyName, override);
|
|
4238
|
-
return sealCredential({
|
|
4239
|
-
apiKey: override,
|
|
4240
|
-
tier: "override",
|
|
4241
|
-
source: overrideKeyName,
|
|
4242
|
-
deliberate: true,
|
|
4243
|
-
deprecated: false,
|
|
4244
|
-
diskCandidates: diskPaths,
|
|
4245
|
-
warning: null
|
|
4246
|
-
});
|
|
4247
|
-
}
|
|
4248
|
-
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4249
|
-
if (profile) {
|
|
4250
|
-
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4251
|
-
if (!SAFE_PROFILE.test(profile)) {
|
|
4252
|
-
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4253
|
-
}
|
|
4254
|
-
const paths = profileDiskSources(name, env, profile);
|
|
4255
|
-
for (const path of paths) {
|
|
4256
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
4257
|
-
if (value) {
|
|
4258
|
-
assertUsableCredential(name, path, value);
|
|
4259
|
-
return sealCredential({
|
|
4260
|
-
apiKey: value,
|
|
4261
|
-
tier: "profile",
|
|
4262
|
-
source: path,
|
|
4263
|
-
deliberate: true,
|
|
4264
|
-
deprecated: false,
|
|
4265
|
-
diskCandidates: paths,
|
|
4266
|
-
warning: null
|
|
4267
|
-
});
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
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);
|
|
4271
|
-
}
|
|
4272
|
-
const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4273
|
-
if (diskHits.length > 0) {
|
|
4274
|
-
const winner = diskHits[0];
|
|
4275
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
4276
|
-
const divergentSources = [
|
|
4277
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
4278
|
-
...(() => {
|
|
4279
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4280
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4281
|
-
})()
|
|
4282
|
-
];
|
|
4283
|
-
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.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;
|
|
4284
|
-
return sealCredential({
|
|
4285
|
-
apiKey: winner.value,
|
|
4286
|
-
tier: "disk",
|
|
4287
|
-
source: winner.path,
|
|
4288
|
-
deliberate: false,
|
|
4289
|
-
deprecated: false,
|
|
4290
|
-
diskCandidates: diskPaths,
|
|
4291
|
-
warning
|
|
4292
|
-
});
|
|
4293
|
-
}
|
|
4294
|
-
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
4295
|
-
if (legacy) {
|
|
4296
|
-
assertUsableCredential(name, legacy.key, legacy.value);
|
|
4297
|
-
const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
|
|
4298
|
-
const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
|
|
4299
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4300
|
-
const notified = deprecationNotified();
|
|
4301
|
-
if (!notified.has(name)) {
|
|
4302
|
-
notified.add(name);
|
|
4303
|
-
sink(message);
|
|
4304
|
-
}
|
|
4305
|
-
return sealCredential({
|
|
4306
|
-
apiKey: legacy.value,
|
|
4307
|
-
tier: "legacy-env",
|
|
4308
|
-
source: legacy.key,
|
|
4309
|
-
deliberate: false,
|
|
4310
|
-
deprecated: true,
|
|
4311
|
-
diskCandidates: diskPaths,
|
|
4312
|
-
warning: message
|
|
4313
|
-
});
|
|
4314
|
-
}
|
|
4315
|
-
return null;
|
|
4316
|
-
}
|
|
4317
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
4318
|
-
var init_transport = __esm(() => {
|
|
4319
|
-
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
4320
|
-
appName;
|
|
4321
|
-
attempted;
|
|
4322
|
-
constructor(appName, message, attempted) {
|
|
4323
|
-
super(message);
|
|
4324
|
-
this.name = "CredentialResolutionError";
|
|
4325
|
-
this.appName = appName;
|
|
4326
|
-
this.attempted = attempted;
|
|
4327
|
-
}
|
|
4328
|
-
};
|
|
4329
|
-
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4330
|
-
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4331
|
-
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4332
|
-
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4333
|
-
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4334
|
-
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4335
|
-
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4336
|
-
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4337
|
-
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4338
|
-
"host",
|
|
4339
|
-
":authority",
|
|
4340
|
-
"forwarded",
|
|
4341
|
-
"x-forwarded-host",
|
|
4342
|
-
"x-original-host"
|
|
4343
|
-
]);
|
|
4344
|
-
});
|
|
4345
|
-
|
|
4346
4190
|
// src/config.ts
|
|
4347
|
-
|
|
4348
|
-
__export(exports_config, {
|
|
4349
|
-
writeConfig: () => writeConfig,
|
|
4350
|
-
resolveApiUrl: () => resolveApiUrl,
|
|
4351
|
-
resolveApiKey: () => resolveApiKey,
|
|
4352
|
-
readConfig: () => readConfig,
|
|
4353
|
-
getLockPath: () => getLockPath,
|
|
4354
|
-
getHooksDataDir: () => getHooksDataDir,
|
|
4355
|
-
getCustomHooksDir: () => getCustomHooksDir,
|
|
4356
|
-
getConfigPath: () => getConfigPath
|
|
4357
|
-
});
|
|
4358
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4359
|
-
import { join as join3 } from "path";
|
|
4360
|
-
import { homedir as homedir2 } from "os";
|
|
4191
|
+
import { join as join5 } from "path";
|
|
4361
4192
|
function getHooksDataDir() {
|
|
4362
|
-
|
|
4363
|
-
if (explicit)
|
|
4364
|
-
return explicit;
|
|
4365
|
-
return join3(homedir2(), ".hasna", "hooks");
|
|
4193
|
+
return getEffectiveDataRoot();
|
|
4366
4194
|
}
|
|
4367
4195
|
function getCustomHooksDir() {
|
|
4368
|
-
return
|
|
4196
|
+
return join5(getHooksDataDir(), "hooks");
|
|
4369
4197
|
}
|
|
4370
4198
|
function getLockPath() {
|
|
4371
4199
|
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4372
4200
|
if (explicit)
|
|
4373
4201
|
return explicit;
|
|
4374
|
-
return
|
|
4375
|
-
}
|
|
4376
|
-
function getConfigPath() {
|
|
4377
|
-
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4378
|
-
if (explicit)
|
|
4379
|
-
return explicit;
|
|
4380
|
-
return join3(getHooksDataDir(), "config.json");
|
|
4381
|
-
}
|
|
4382
|
-
function readConfig() {
|
|
4383
|
-
try {
|
|
4384
|
-
const path = getConfigPath();
|
|
4385
|
-
if (!existsSync2(path))
|
|
4386
|
-
return {};
|
|
4387
|
-
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
4388
|
-
} catch {
|
|
4389
|
-
return {};
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
function writeConfig(config) {
|
|
4393
|
-
const path = getConfigPath();
|
|
4394
|
-
mkdirSync(join3(path, ".."), { recursive: true });
|
|
4395
|
-
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4396
|
-
`, "utf-8");
|
|
4397
|
-
return path;
|
|
4398
|
-
}
|
|
4399
|
-
function resolveApiUrl() {
|
|
4400
|
-
const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
|
|
4401
|
-
if (envUrl)
|
|
4402
|
-
return envUrl.replace(/\/+$/, "");
|
|
4403
|
-
const cfg = readConfig();
|
|
4404
|
-
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4405
|
-
}
|
|
4406
|
-
function resolveApiKey() {
|
|
4407
|
-
return resolveCredential("hooks", process.env)?.apiKey;
|
|
4202
|
+
return join5(getHooksDataDir(), "hooks.lock");
|
|
4408
4203
|
}
|
|
4409
4204
|
var init_config = __esm(() => {
|
|
4410
|
-
|
|
4205
|
+
init_app_home();
|
|
4411
4206
|
});
|
|
4412
4207
|
|
|
4413
4208
|
// src/lib/semver.ts
|
|
@@ -4434,8 +4229,8 @@ __export(exports_manifest, {
|
|
|
4434
4229
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
4435
4230
|
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4436
4231
|
});
|
|
4437
|
-
import { existsSync as
|
|
4438
|
-
import { basename, dirname, isAbsolute, join as
|
|
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";
|
|
4439
4234
|
import { z } from "zod";
|
|
4440
4235
|
function parseManifest(json) {
|
|
4441
4236
|
const parsed = JSON.parse(json);
|
|
@@ -4451,8 +4246,8 @@ function isWithin(child, parent) {
|
|
|
4451
4246
|
return child === parent || child.startsWith(parent + sep);
|
|
4452
4247
|
}
|
|
4453
4248
|
function assertContainedLexical(target, root, raw) {
|
|
4454
|
-
const lexical =
|
|
4455
|
-
const rootLexical =
|
|
4249
|
+
const lexical = resolve3(target);
|
|
4250
|
+
const rootLexical = resolve3(root);
|
|
4456
4251
|
if (!isWithin(lexical, rootLexical)) {
|
|
4457
4252
|
throw new ScriptContainmentError(target, root, raw);
|
|
4458
4253
|
}
|
|
@@ -4461,14 +4256,14 @@ function assertContainedLexical(target, root, raw) {
|
|
|
4461
4256
|
function realpathNearest(path) {
|
|
4462
4257
|
const tail = [];
|
|
4463
4258
|
let cur = path;
|
|
4464
|
-
while (!
|
|
4259
|
+
while (!existsSync5(cur)) {
|
|
4465
4260
|
const parent = dirname(cur);
|
|
4466
4261
|
if (parent === cur)
|
|
4467
4262
|
break;
|
|
4468
4263
|
tail.unshift(basename(cur));
|
|
4469
4264
|
cur = parent;
|
|
4470
4265
|
}
|
|
4471
|
-
return
|
|
4266
|
+
return join6(realpathSync(cur), ...tail);
|
|
4472
4267
|
}
|
|
4473
4268
|
function assertContainedReal(root, target, raw) {
|
|
4474
4269
|
const rootReal = realpathSync(root);
|
|
@@ -4501,25 +4296,25 @@ function resolveScript(manifest, manifestDir) {
|
|
|
4501
4296
|
if (isInlineScript(manifest))
|
|
4502
4297
|
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4503
4298
|
const rel = scriptRelFor(manifest);
|
|
4504
|
-
const resolved = assertContained(
|
|
4505
|
-
if (!
|
|
4299
|
+
const resolved = assertContained(join6(manifestDir, rel), manifestDir, manifest.script);
|
|
4300
|
+
if (!existsSync5(resolved)) {
|
|
4506
4301
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4507
4302
|
}
|
|
4508
|
-
return { path: rel, content:
|
|
4303
|
+
return { path: rel, content: readFileSync(resolved, "utf-8") };
|
|
4509
4304
|
}
|
|
4510
4305
|
function customHookDir(name) {
|
|
4511
|
-
return
|
|
4306
|
+
return join6(getCustomHooksDir(), shortManifestName(name));
|
|
4512
4307
|
}
|
|
4513
4308
|
function readCustomManifest(name) {
|
|
4514
4309
|
const dir = customHookDir(name);
|
|
4515
|
-
const manifestPath =
|
|
4516
|
-
if (!
|
|
4310
|
+
const manifestPath = join6(dir, "manifest.json");
|
|
4311
|
+
if (!existsSync5(manifestPath))
|
|
4517
4312
|
return;
|
|
4518
4313
|
try {
|
|
4519
|
-
const manifest = parseManifest(
|
|
4314
|
+
const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
|
|
4520
4315
|
const script = resolveScript(manifest, dir);
|
|
4521
4316
|
const scriptIsInline = isInlineScript(manifest);
|
|
4522
|
-
return { manifest, scriptPath:
|
|
4317
|
+
return { manifest, scriptPath: join6(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4523
4318
|
} catch (err) {
|
|
4524
4319
|
if (err instanceof ScriptContainmentError)
|
|
4525
4320
|
throw err;
|
|
@@ -4528,7 +4323,7 @@ function readCustomManifest(name) {
|
|
|
4528
4323
|
}
|
|
4529
4324
|
function listCustomHooks() {
|
|
4530
4325
|
const dir = getCustomHooksDir();
|
|
4531
|
-
if (!
|
|
4326
|
+
if (!existsSync5(dir))
|
|
4532
4327
|
return [];
|
|
4533
4328
|
const results = [];
|
|
4534
4329
|
for (const entry of readdirSafe(dir)) {
|
|
@@ -4543,15 +4338,15 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4543
4338
|
if (isAbsolute(scriptRelPath)) {
|
|
4544
4339
|
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4545
4340
|
}
|
|
4546
|
-
const scriptPath = scriptRelPath.includes("/") ?
|
|
4341
|
+
const scriptPath = scriptRelPath.includes("/") ? join6(dir, scriptRelPath) : join6(dir, scriptRelPath);
|
|
4547
4342
|
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4548
|
-
|
|
4343
|
+
mkdirSync(dir, { recursive: true });
|
|
4549
4344
|
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4550
4345
|
const scriptDir = dirname(scriptPath);
|
|
4551
4346
|
if (scriptDir !== dir)
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4347
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
4348
|
+
writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
4349
|
+
writeFileSync(join6(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4555
4350
|
`, "utf-8");
|
|
4556
4351
|
return { dir, scriptPath };
|
|
4557
4352
|
}
|
|
@@ -4595,14 +4390,14 @@ __export(exports_resolve, {
|
|
|
4595
4390
|
resolveHook: () => resolveHook,
|
|
4596
4391
|
resolveBundledHooksDir: () => resolveBundledHooksDir
|
|
4597
4392
|
});
|
|
4598
|
-
import { dirname as dirname2, join as
|
|
4599
|
-
import { existsSync as
|
|
4393
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
4394
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4600
4395
|
import { fileURLToPath } from "url";
|
|
4601
4396
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
4602
|
-
const sourceLayout =
|
|
4603
|
-
if (
|
|
4397
|
+
const sourceLayout = join7(moduleDir, "..", "..", "hooks");
|
|
4398
|
+
if (existsSync6(join7(sourceLayout, "hook-gitguard")))
|
|
4604
4399
|
return sourceLayout;
|
|
4605
|
-
return
|
|
4400
|
+
return join7(moduleDir, "..", "hooks");
|
|
4606
4401
|
}
|
|
4607
4402
|
function resolveHookDir(name) {
|
|
4608
4403
|
const custom = readCustomManifest(name);
|
|
@@ -4613,11 +4408,11 @@ function resolveHookDir(name) {
|
|
|
4613
4408
|
return;
|
|
4614
4409
|
const short = shortManifestName(name);
|
|
4615
4410
|
const bundledBase = resolveBundledHooksDir();
|
|
4616
|
-
const direct =
|
|
4617
|
-
if (
|
|
4411
|
+
const direct = join7(bundledBase, short);
|
|
4412
|
+
if (existsSync6(direct))
|
|
4618
4413
|
return direct;
|
|
4619
|
-
const prefixed =
|
|
4620
|
-
return
|
|
4414
|
+
const prefixed = join7(bundledBase, `hook-${short}`);
|
|
4415
|
+
return existsSync6(prefixed) ? prefixed : undefined;
|
|
4621
4416
|
}
|
|
4622
4417
|
function resolveScriptPath(name) {
|
|
4623
4418
|
const custom = readCustomManifest(name);
|
|
@@ -4626,8 +4421,8 @@ function resolveScriptPath(name) {
|
|
|
4626
4421
|
const dir = resolveHookDir(name);
|
|
4627
4422
|
if (!dir)
|
|
4628
4423
|
return;
|
|
4629
|
-
const script =
|
|
4630
|
-
return
|
|
4424
|
+
const script = join7(dir, "src", "hook.ts");
|
|
4425
|
+
return existsSync6(script) ? script : undefined;
|
|
4631
4426
|
}
|
|
4632
4427
|
function resolveHookMeta(name) {
|
|
4633
4428
|
const custom = readCustomManifest(name);
|
|
@@ -4876,8 +4671,8 @@ var init_migrations = __esm(() => {
|
|
|
4876
4671
|
});
|
|
4877
4672
|
|
|
4878
4673
|
// src/db/legacy-import.ts
|
|
4879
|
-
import { existsSync as
|
|
4880
|
-
import { join as
|
|
4674
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
4675
|
+
import { join as join8 } from "path";
|
|
4881
4676
|
import { homedir as homedir3 } from "os";
|
|
4882
4677
|
function ensureMetaTable(db) {
|
|
4883
4678
|
db.exec(`
|
|
@@ -4901,7 +4696,7 @@ function nanoid() {
|
|
|
4901
4696
|
function importJsonlFile(db, filePath) {
|
|
4902
4697
|
let count = 0;
|
|
4903
4698
|
try {
|
|
4904
|
-
const lines =
|
|
4699
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
4905
4700
|
`).filter(Boolean);
|
|
4906
4701
|
for (const line of lines) {
|
|
4907
4702
|
try {
|
|
@@ -4927,7 +4722,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4927
4722
|
function importErrorsLog(db, filePath) {
|
|
4928
4723
|
let count = 0;
|
|
4929
4724
|
try {
|
|
4930
|
-
const lines =
|
|
4725
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
4931
4726
|
`).filter(Boolean);
|
|
4932
4727
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4933
4728
|
for (const line of lines) {
|
|
@@ -4952,25 +4747,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4952
4747
|
} catch {}
|
|
4953
4748
|
return count;
|
|
4954
4749
|
}
|
|
4955
|
-
function runLegacyImport(db,
|
|
4750
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
4956
4751
|
try {
|
|
4957
4752
|
if (isAlreadyDone(db))
|
|
4958
4753
|
return;
|
|
4959
4754
|
let total = 0;
|
|
4960
|
-
const claudeProjectsDir =
|
|
4961
|
-
if (
|
|
4755
|
+
const claudeProjectsDir = join8(homeDir, ".claude", "projects");
|
|
4756
|
+
if (existsSync7(claudeProjectsDir)) {
|
|
4962
4757
|
try {
|
|
4963
4758
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4964
4759
|
for (const dir of projectDirs) {
|
|
4965
|
-
const projectDir =
|
|
4760
|
+
const projectDir = join8(claudeProjectsDir, dir);
|
|
4966
4761
|
try {
|
|
4967
4762
|
const files = readdirSync2(projectDir);
|
|
4968
4763
|
for (const file of files) {
|
|
4969
4764
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4970
|
-
total += importJsonlFile(db,
|
|
4765
|
+
total += importJsonlFile(db, join8(projectDir, file));
|
|
4971
4766
|
}
|
|
4972
4767
|
if (file === "errors.log") {
|
|
4973
|
-
total += importErrorsLog(db,
|
|
4768
|
+
total += importErrorsLog(db, join8(projectDir, file));
|
|
4974
4769
|
}
|
|
4975
4770
|
}
|
|
4976
4771
|
} catch {}
|
|
@@ -5013,39 +4808,35 @@ __export(exports_db, {
|
|
|
5013
4808
|
closeDb: () => closeDb
|
|
5014
4809
|
});
|
|
5015
4810
|
import { Database } from "bun:sqlite";
|
|
5016
|
-
import { existsSync as
|
|
5017
|
-
import { join as
|
|
5018
|
-
import { homedir as homedir4 } from "os";
|
|
4811
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
4812
|
+
import { join as join9 } from "path";
|
|
5019
4813
|
function resolveDataDir() {
|
|
5020
|
-
const
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
5026
|
-
mkdirSync3(join7(homedir4(), ".hasna"), { recursive: true });
|
|
5027
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
4814
|
+
const effective = getEffectiveDataRoot();
|
|
4815
|
+
const oldDir = join9(getHomeDir(), ".hooks");
|
|
4816
|
+
if (!existsSync8(effective) && existsSync8(oldDir)) {
|
|
4817
|
+
mkdirSync2(effective, { recursive: true });
|
|
4818
|
+
cpSync(oldDir, effective, { recursive: true });
|
|
5028
4819
|
}
|
|
5029
|
-
return
|
|
4820
|
+
return effective;
|
|
5030
4821
|
}
|
|
5031
4822
|
function getDbPath() {
|
|
5032
4823
|
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
5033
4824
|
if (explicitDb)
|
|
5034
4825
|
return explicitDb;
|
|
5035
|
-
const
|
|
5036
|
-
return
|
|
4826
|
+
const dataDir3 = resolveDataDir();
|
|
4827
|
+
return join9(dataDir3, "hooks.db");
|
|
5037
4828
|
}
|
|
5038
4829
|
function ensureDir(dbPath) {
|
|
5039
4830
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5040
|
-
if (dir && !
|
|
5041
|
-
|
|
4831
|
+
if (dir && !existsSync8(dir)) {
|
|
4832
|
+
mkdirSync2(dir, { recursive: true });
|
|
5042
4833
|
}
|
|
5043
4834
|
}
|
|
5044
4835
|
function getDb() {
|
|
5045
4836
|
if (instance)
|
|
5046
4837
|
return instance;
|
|
5047
4838
|
const dbPath = getDbPath();
|
|
5048
|
-
const isNew = dbPath === ":memory:" || !
|
|
4839
|
+
const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
|
|
5049
4840
|
ensureDir(dbPath);
|
|
5050
4841
|
instance = new Database(dbPath);
|
|
5051
4842
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -5081,6 +4872,7 @@ function createTestDb() {
|
|
|
5081
4872
|
}
|
|
5082
4873
|
var instance = null;
|
|
5083
4874
|
var init_db = __esm(() => {
|
|
4875
|
+
init_app_home();
|
|
5084
4876
|
init_migrations();
|
|
5085
4877
|
init_legacy_import();
|
|
5086
4878
|
});
|
|
@@ -5107,7 +4899,7 @@ __export(exports_store, {
|
|
|
5107
4899
|
LockFileError: () => LockFileError
|
|
5108
4900
|
});
|
|
5109
4901
|
import { createHash } from "crypto";
|
|
5110
|
-
import { existsSync as
|
|
4902
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5111
4903
|
import { readFile as readFile2 } from "fs/promises";
|
|
5112
4904
|
import { randomBytes } from "crypto";
|
|
5113
4905
|
function sha256Of(input) {
|
|
@@ -5169,11 +4961,11 @@ function assertLockShape(parsed, path) {
|
|
|
5169
4961
|
}
|
|
5170
4962
|
function readLock() {
|
|
5171
4963
|
const path = getLockPath();
|
|
5172
|
-
if (!
|
|
4964
|
+
if (!existsSync9(path))
|
|
5173
4965
|
return { hooks: {} };
|
|
5174
4966
|
let raw;
|
|
5175
4967
|
try {
|
|
5176
|
-
raw =
|
|
4968
|
+
raw = readFileSync3(path, "utf-8");
|
|
5177
4969
|
} catch {
|
|
5178
4970
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5179
4971
|
}
|
|
@@ -5188,13 +4980,13 @@ function readLock() {
|
|
|
5188
4980
|
}
|
|
5189
4981
|
function writeLock(lock) {
|
|
5190
4982
|
const path = getLockPath();
|
|
5191
|
-
|
|
4983
|
+
mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5192
4984
|
const sorted = {};
|
|
5193
4985
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5194
4986
|
sorted[name] = lock.hooks[name];
|
|
5195
4987
|
}
|
|
5196
4988
|
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
5197
|
-
|
|
4989
|
+
writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5198
4990
|
`, "utf-8");
|
|
5199
4991
|
renameSync(tmp, path);
|
|
5200
4992
|
return path;
|
|
@@ -5270,7 +5062,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5270
5062
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5271
5063
|
}
|
|
5272
5064
|
function retrustHook(name, scriptPath, version, source) {
|
|
5273
|
-
const actual = sha256Of(
|
|
5065
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5274
5066
|
const db = getDb();
|
|
5275
5067
|
upsertHookRecord(db, {
|
|
5276
5068
|
name,
|
|
@@ -5311,9 +5103,9 @@ __export(exports_installer, {
|
|
|
5311
5103
|
getHookPath: () => getHookPath,
|
|
5312
5104
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5313
5105
|
});
|
|
5314
|
-
import { existsSync as
|
|
5315
|
-
import { join as
|
|
5316
|
-
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";
|
|
5317
5109
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5318
5110
|
function normalizeHookName(name) {
|
|
5319
5111
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -5352,25 +5144,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5352
5144
|
return globalOverride;
|
|
5353
5145
|
const dir = getTargetSettingsDir(target);
|
|
5354
5146
|
if (scope === "project") {
|
|
5355
|
-
return target === "codewith" ?
|
|
5147
|
+
return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
|
|
5356
5148
|
}
|
|
5357
|
-
return target === "codewith" ?
|
|
5149
|
+
return target === "codewith" ? join10(homedir4(), dir, "config.toml") : join10(homedir4(), dir, "settings.json");
|
|
5358
5150
|
}
|
|
5359
5151
|
function getHookPath(name) {
|
|
5360
5152
|
const shortName = shortHookName(name);
|
|
5361
5153
|
const resolved = resolveHookDir(shortName);
|
|
5362
5154
|
if (resolved)
|
|
5363
5155
|
return resolved;
|
|
5364
|
-
return
|
|
5156
|
+
return join10(HOOKS_DIR, normalizeHookName(shortName));
|
|
5365
5157
|
}
|
|
5366
5158
|
function hookExists(name) {
|
|
5367
|
-
return
|
|
5159
|
+
return existsSync10(getHookPath(name));
|
|
5368
5160
|
}
|
|
5369
5161
|
function readSettings(scope = "global", target = "claude") {
|
|
5370
5162
|
const path = getSettingsPath(scope, target);
|
|
5371
5163
|
try {
|
|
5372
|
-
if (
|
|
5373
|
-
return JSON.parse(
|
|
5164
|
+
if (existsSync10(path)) {
|
|
5165
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
5374
5166
|
}
|
|
5375
5167
|
} catch (error) {
|
|
5376
5168
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5380,10 +5172,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
5380
5172
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
5381
5173
|
const path = getSettingsPath(scope, target);
|
|
5382
5174
|
const dir = dirname3(path);
|
|
5383
|
-
if (!
|
|
5384
|
-
|
|
5175
|
+
if (!existsSync10(dir)) {
|
|
5176
|
+
mkdirSync4(dir, { recursive: true });
|
|
5385
5177
|
}
|
|
5386
|
-
|
|
5178
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
5387
5179
|
`);
|
|
5388
5180
|
}
|
|
5389
5181
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -5469,7 +5261,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
5469
5261
|
function readCodewithConfig(scope, configPath) {
|
|
5470
5262
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5471
5263
|
try {
|
|
5472
|
-
return
|
|
5264
|
+
return existsSync10(path) ? readFileSync4(path, "utf-8") : "";
|
|
5473
5265
|
} catch {
|
|
5474
5266
|
return "";
|
|
5475
5267
|
}
|
|
@@ -5477,13 +5269,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
5477
5269
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5478
5270
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5479
5271
|
const dir = dirname3(path);
|
|
5480
|
-
if (!
|
|
5481
|
-
|
|
5482
|
-
const existing =
|
|
5272
|
+
if (!existsSync10(dir))
|
|
5273
|
+
mkdirSync4(dir, { recursive: true });
|
|
5274
|
+
const existing = existsSync10(path) ? readFileSync4(path, "utf-8") : "";
|
|
5483
5275
|
const sep2 = existing.trim() ? `
|
|
5484
5276
|
|
|
5485
5277
|
` : "";
|
|
5486
|
-
|
|
5278
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
5487
5279
|
return path;
|
|
5488
5280
|
}
|
|
5489
5281
|
function detectConflict(name, scope, target) {
|
|
@@ -5779,14 +5571,28 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
5779
5571
|
}
|
|
5780
5572
|
function codewithHasHookEntry(name, scope = "global") {
|
|
5781
5573
|
const path = getSettingsPath(scope, "codewith");
|
|
5782
|
-
if (!
|
|
5574
|
+
if (!existsSync10(path))
|
|
5783
5575
|
return false;
|
|
5784
5576
|
try {
|
|
5785
|
-
return codewithHasHookEntryInText(
|
|
5577
|
+
return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
|
|
5786
5578
|
} catch {
|
|
5787
5579
|
return false;
|
|
5788
5580
|
}
|
|
5789
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
|
+
}
|
|
5790
5596
|
function uninstallHook(name, scope = "global", target = "claude") {
|
|
5791
5597
|
const shortName = shortHookName(name);
|
|
5792
5598
|
const custom = readCustomManifest(shortName);
|
|
@@ -5813,11 +5619,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5813
5619
|
const registrationsRemaining = [];
|
|
5814
5620
|
if (target === "codewith" || target === "all") {
|
|
5815
5621
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5816
|
-
if (
|
|
5817
|
-
const before =
|
|
5622
|
+
if (existsSync10(codewithPath)) {
|
|
5623
|
+
const before = readFileSync4(codewithPath, "utf-8");
|
|
5818
5624
|
const after = removeCodewithHookEntry(before, shortName);
|
|
5819
5625
|
if (after.removed) {
|
|
5820
|
-
|
|
5626
|
+
writeFileSync3(codewithPath, after.text, "utf-8");
|
|
5821
5627
|
}
|
|
5822
5628
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
5823
5629
|
registrationsRemaining.push("codewith");
|
|
@@ -5826,7 +5632,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5826
5632
|
}
|
|
5827
5633
|
if (custom) {
|
|
5828
5634
|
try {
|
|
5829
|
-
|
|
5635
|
+
removeStoreDirFailClosed(customHookDir(shortName));
|
|
5830
5636
|
} catch {
|
|
5831
5637
|
return {
|
|
5832
5638
|
name: shortName,
|
|
@@ -5860,7 +5666,7 @@ var init_installer = __esm(() => {
|
|
|
5860
5666
|
init_manifest();
|
|
5861
5667
|
init_store();
|
|
5862
5668
|
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5863
|
-
HOOKS_DIR =
|
|
5669
|
+
HOOKS_DIR = existsSync10(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
|
|
5864
5670
|
EVENT_MAP = {
|
|
5865
5671
|
claude: {
|
|
5866
5672
|
PreToolUse: "PreToolUse",
|
|
@@ -5961,154 +5767,1030 @@ function redactValue2(value) {
|
|
|
5961
5767
|
}
|
|
5962
5768
|
return out;
|
|
5963
5769
|
}
|
|
5964
|
-
return value;
|
|
5965
|
-
}
|
|
5966
|
-
function redactEventPayload(text) {
|
|
5967
|
-
if (text === null || text === undefined)
|
|
5968
|
-
return null;
|
|
5969
|
-
try {
|
|
5970
|
-
const parsed = JSON.parse(text);
|
|
5971
|
-
return redactText(JSON.stringify(redactValue2(parsed)));
|
|
5972
|
-
} catch {
|
|
5973
|
-
return redactText(text);
|
|
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)
|
|
5888
|
+
return;
|
|
5889
|
+
profile.last_seen_at = new Date().toISOString();
|
|
5890
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5891
|
+
`);
|
|
5892
|
+
}
|
|
5893
|
+
function exportProfiles() {
|
|
5894
|
+
return listProfiles();
|
|
5895
|
+
}
|
|
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;
|
|
5904
|
+
}
|
|
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++;
|
|
5913
|
+
}
|
|
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))
|
|
5968
|
+
return [];
|
|
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;
|
|
6001
|
+
}
|
|
6002
|
+
value = value.slice(1, -1);
|
|
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);
|
|
6011
|
+
}
|
|
6012
|
+
return { values, unusable };
|
|
6013
|
+
}
|
|
6014
|
+
function configFileModeAllowed(mode) {
|
|
6015
|
+
const permissions = mode & 4095;
|
|
6016
|
+
return permissions === 256 || permissions === 384;
|
|
6017
|
+
}
|
|
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"})`);
|
|
6035
|
+
}
|
|
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);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
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
|
+
}
|
|
6067
|
+
}
|
|
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;
|
|
6073
|
+
}
|
|
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)
|
|
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 };
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
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]);
|
|
6101
|
+
}
|
|
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
|
|
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;
|
|
6153
|
+
}
|
|
6154
|
+
function isAmbientEnvironment(env) {
|
|
6155
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
6156
|
+
}
|
|
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
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
function keychainTierEnabled(env, options) {
|
|
6170
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
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;
|
|
6201
|
+
try {
|
|
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
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
return Object.freeze(snapshot);
|
|
6257
|
+
}
|
|
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
|
+
});
|
|
6276
|
+
}
|
|
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]);
|
|
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
|
+
});
|
|
6293
|
+
}
|
|
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]);
|
|
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
|
+
});
|
|
6313
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
6343
|
+
}
|
|
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
|
+
});
|
|
5974
6395
|
}
|
|
6396
|
+
return null;
|
|
5975
6397
|
}
|
|
5976
|
-
function
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
else if (value === null)
|
|
5983
|
-
out[field] = null;
|
|
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)) {
|
|
6403
|
+
return false;
|
|
5984
6404
|
}
|
|
5985
|
-
return
|
|
6405
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
5986
6406
|
}
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
SECRET_SHAPES = [
|
|
5991
|
-
/\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
|
|
5992
|
-
/\bsk-[A-Za-z0-9]{16,}/g,
|
|
5993
|
-
/\btpe_[A-Za-z0-9]{8,}/g,
|
|
5994
|
-
/\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
5995
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
5996
|
-
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
5997
|
-
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
5998
|
-
/AKIA[0-9A-Z]{16}/g,
|
|
5999
|
-
/ASIA[0-9A-Z]{16}/g,
|
|
6000
|
-
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
6001
|
-
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
6002
|
-
/-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
6003
|
-
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
6004
|
-
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
6005
|
-
/\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
|
|
6006
|
-
];
|
|
6007
|
-
});
|
|
6008
|
-
|
|
6009
|
-
// src/lib/profiles.ts
|
|
6010
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
6011
|
-
import { join as join9 } from "path";
|
|
6012
|
-
import { homedir as homedir6 } from "os";
|
|
6013
|
-
function resolveProfilesDir() {
|
|
6014
|
-
const newDir = join9(homedir6(), ".hasna", "hooks", "profiles");
|
|
6015
|
-
const oldDir = join9(homedir6(), ".hooks", "profiles");
|
|
6016
|
-
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
6017
|
-
mkdirSync6(join9(homedir6(), ".hasna", "hooks"), { recursive: true });
|
|
6018
|
-
cpSync2(oldDir, newDir, { recursive: true });
|
|
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.");
|
|
6019
6410
|
}
|
|
6020
|
-
return
|
|
6411
|
+
return name;
|
|
6021
6412
|
}
|
|
6022
|
-
function
|
|
6023
|
-
|
|
6024
|
-
|
|
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;
|
|
6423
|
+
}
|
|
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.");
|
|
6025
6431
|
}
|
|
6026
6432
|
}
|
|
6027
|
-
function
|
|
6028
|
-
|
|
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.");
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
return rawHostname.toLowerCase();
|
|
6029
6472
|
}
|
|
6030
|
-
function
|
|
6031
|
-
return
|
|
6473
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
6474
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
6032
6475
|
}
|
|
6033
|
-
function
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
const now2 = new Date().toISOString();
|
|
6037
|
-
const profile = {
|
|
6038
|
-
agent_id: id,
|
|
6039
|
-
agent_type: input.agent_type,
|
|
6040
|
-
created_at: now2,
|
|
6041
|
-
last_seen_at: now2,
|
|
6042
|
-
preferences: {}
|
|
6043
|
-
};
|
|
6044
|
-
if (input.name) {
|
|
6045
|
-
profile.name = input.name;
|
|
6476
|
+
function toV1BaseUrl(apiUrl) {
|
|
6477
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
6478
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
6046
6479
|
}
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
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(/\/+$/, "");
|
|
6050
6513
|
}
|
|
6051
|
-
function
|
|
6052
|
-
|
|
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() }] : []
|
|
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;
|
|
6053
6572
|
try {
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
return null;
|
|
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]);
|
|
6059
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
|
+
};
|
|
6060
6592
|
}
|
|
6061
|
-
function
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
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;
|
|
6072
6610
|
}
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
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
|
+
]);
|
|
6651
|
+
});
|
|
6652
|
+
|
|
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() !== "");
|
|
6656
|
+
}
|
|
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
|
+
];
|
|
6666
|
+
}
|
|
6667
|
+
function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
6668
|
+
return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
6669
|
+
}
|
|
6670
|
+
function selectsHooksLocalStore(env = process.env) {
|
|
6671
|
+
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
6672
|
+
}
|
|
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;
|
|
6681
|
+
}
|
|
6682
|
+
function isAmbientHooksEnv(env) {
|
|
6683
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
6684
|
+
return true;
|
|
6685
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
6686
|
+
}
|
|
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);
|
|
6076
6694
|
}
|
|
6695
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
6077
6696
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
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;
|
|
6716
|
+
}
|
|
6717
|
+
function announceLocal(notice, reason) {
|
|
6718
|
+
if (localNoticePrinted)
|
|
6081
6719
|
return;
|
|
6082
|
-
|
|
6083
|
-
|
|
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}
|
|
6084
6726
|
`);
|
|
6085
6727
|
}
|
|
6086
|
-
function
|
|
6087
|
-
|
|
6728
|
+
function announceHooksLocalMode(reason = "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority", notice) {
|
|
6729
|
+
announceLocal(notice, reason);
|
|
6088
6730
|
}
|
|
6089
|
-
function
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
if (
|
|
6100
|
-
|
|
6101
|
-
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 });
|
|
6102
6743
|
}
|
|
6103
|
-
|
|
6104
|
-
`);
|
|
6105
|
-
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 });
|
|
6106
6745
|
}
|
|
6107
|
-
|
|
6746
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
6108
6747
|
}
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
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();
|
|
6112
6794
|
});
|
|
6113
6795
|
|
|
6114
6796
|
// src/lib/db-writer.ts
|
|
@@ -6208,8 +6890,8 @@ var init_db_writer = __esm(() => {
|
|
|
6208
6890
|
});
|
|
6209
6891
|
|
|
6210
6892
|
// src/lib/hook-env.ts
|
|
6211
|
-
import { dirname as dirname4, isAbsolute as
|
|
6212
|
-
import { existsSync as
|
|
6893
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
6894
|
+
import { existsSync as existsSync12, statSync } from "fs";
|
|
6213
6895
|
function isDeniedEnvName(name) {
|
|
6214
6896
|
const upper = name.toUpperCase();
|
|
6215
6897
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -6227,7 +6909,7 @@ function isBashFunctionEnvName(name) {
|
|
|
6227
6909
|
return name.startsWith("BASH_FUNC_");
|
|
6228
6910
|
}
|
|
6229
6911
|
function isUnsafePathEntry(entry, home) {
|
|
6230
|
-
if (entry === "" || !
|
|
6912
|
+
if (entry === "" || !isAbsolute3(entry))
|
|
6231
6913
|
return true;
|
|
6232
6914
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
6233
6915
|
return true;
|
|
@@ -6236,14 +6918,14 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6236
6918
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
6237
6919
|
return true;
|
|
6238
6920
|
try {
|
|
6239
|
-
if ((
|
|
6921
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
6240
6922
|
return true;
|
|
6241
6923
|
} catch {}
|
|
6242
6924
|
return false;
|
|
6243
6925
|
}
|
|
6244
6926
|
function sanitizeHookPath(pathValue, home) {
|
|
6245
6927
|
const baseline = PATH_BASELINE.slice();
|
|
6246
|
-
if (process.platform === "darwin" &&
|
|
6928
|
+
if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
|
|
6247
6929
|
baseline.push("/opt/homebrew/bin");
|
|
6248
6930
|
const bunDir = dirname4(process.execPath);
|
|
6249
6931
|
const seen = new Set;
|
|
@@ -6368,8 +7050,8 @@ __export(exports_run, {
|
|
|
6368
7050
|
HookTimeoutError: () => HookTimeoutError
|
|
6369
7051
|
});
|
|
6370
7052
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6371
|
-
import { rmSync as
|
|
6372
|
-
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";
|
|
6373
7055
|
function extensionOf(scriptPath) {
|
|
6374
7056
|
const base = basename2(scriptPath);
|
|
6375
7057
|
const dot = base.lastIndexOf(".");
|
|
@@ -6440,7 +7122,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6440
7122
|
async function executeVerifiedScript(options) {
|
|
6441
7123
|
const scriptDir = dirname5(options.scriptPath);
|
|
6442
7124
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6443
|
-
const tempPath =
|
|
7125
|
+
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6444
7126
|
let proc = null;
|
|
6445
7127
|
let timedOut = false;
|
|
6446
7128
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6448,8 +7130,8 @@ async function executeVerifiedScript(options) {
|
|
|
6448
7130
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
6449
7131
|
}, options.timeout) : null;
|
|
6450
7132
|
try {
|
|
6451
|
-
|
|
6452
|
-
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 ?? []], {
|
|
6453
7135
|
stdin: new Response(options.stdin),
|
|
6454
7136
|
stdout: "pipe",
|
|
6455
7137
|
stderr: "pipe",
|
|
@@ -6470,7 +7152,7 @@ async function executeVerifiedScript(options) {
|
|
|
6470
7152
|
if (timeoutTimer)
|
|
6471
7153
|
clearTimeout(timeoutTimer);
|
|
6472
7154
|
try {
|
|
6473
|
-
|
|
7155
|
+
rmSync2(tempPath, { force: true });
|
|
6474
7156
|
} catch {}
|
|
6475
7157
|
}
|
|
6476
7158
|
}
|
|
@@ -6531,15 +7213,15 @@ __export(exports_custom_install, {
|
|
|
6531
7213
|
installCustomSource: () => installCustomSource,
|
|
6532
7214
|
ensureCustomDir: () => ensureCustomDir
|
|
6533
7215
|
});
|
|
6534
|
-
import { existsSync as
|
|
6535
|
-
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";
|
|
6536
7218
|
import { tmpdir } from "os";
|
|
6537
7219
|
function manifestAt(dir) {
|
|
6538
|
-
const path =
|
|
6539
|
-
if (!
|
|
7220
|
+
const path = join14(dir, "manifest.json");
|
|
7221
|
+
if (!existsSync13(path))
|
|
6540
7222
|
return;
|
|
6541
7223
|
try {
|
|
6542
|
-
return parseManifest(
|
|
7224
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
6543
7225
|
} catch {
|
|
6544
7226
|
return;
|
|
6545
7227
|
}
|
|
@@ -6558,8 +7240,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
6558
7240
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
6559
7241
|
}
|
|
6560
7242
|
async function cloneToTemp(gitUrl) {
|
|
6561
|
-
const tmp = mkdtempSync(
|
|
6562
|
-
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")], {
|
|
6563
7245
|
stdout: "pipe",
|
|
6564
7246
|
stderr: "pipe"
|
|
6565
7247
|
});
|
|
@@ -6569,10 +7251,10 @@ async function cloneToTemp(gitUrl) {
|
|
|
6569
7251
|
proc.exited
|
|
6570
7252
|
]);
|
|
6571
7253
|
if (exitCode !== 0) {
|
|
6572
|
-
|
|
7254
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
6573
7255
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6574
7256
|
}
|
|
6575
|
-
return
|
|
7257
|
+
return join14(tmp, "repo");
|
|
6576
7258
|
}
|
|
6577
7259
|
function isGitUrl(value) {
|
|
6578
7260
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -6582,7 +7264,7 @@ function isCustomSource(value) {
|
|
|
6582
7264
|
return true;
|
|
6583
7265
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
6584
7266
|
return true;
|
|
6585
|
-
return
|
|
7267
|
+
return existsSync13(value);
|
|
6586
7268
|
}
|
|
6587
7269
|
async function installCustomSource(source) {
|
|
6588
7270
|
if (isGitUrl(source)) {
|
|
@@ -6590,17 +7272,17 @@ async function installCustomSource(source) {
|
|
|
6590
7272
|
try {
|
|
6591
7273
|
return copyManifestDir(repoDir, source, "git");
|
|
6592
7274
|
} finally {
|
|
6593
|
-
|
|
7275
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
6594
7276
|
}
|
|
6595
7277
|
}
|
|
6596
7278
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
6597
7279
|
return installManifestUrl(source);
|
|
6598
7280
|
}
|
|
6599
|
-
if (!
|
|
7281
|
+
if (!existsSync13(source)) {
|
|
6600
7282
|
throw new Error(`No such local path: ${source}`);
|
|
6601
7283
|
}
|
|
6602
7284
|
if (source.endsWith("manifest.json")) {
|
|
6603
|
-
const manifest = parseManifest(
|
|
7285
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
6604
7286
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
6605
7287
|
}
|
|
6606
7288
|
return copyManifestDir(source, source, "local");
|
|
@@ -6631,7 +7313,7 @@ async function installManifestUrl(url) {
|
|
|
6631
7313
|
}
|
|
6632
7314
|
function ensureCustomDir() {
|
|
6633
7315
|
const dir = getCustomHooksDir();
|
|
6634
|
-
|
|
7316
|
+
mkdirSync6(dir, { recursive: true });
|
|
6635
7317
|
return dir;
|
|
6636
7318
|
}
|
|
6637
7319
|
var init_custom_install = __esm(() => {
|
|
@@ -6649,12 +7331,12 @@ __export(exports_sync, {
|
|
|
6649
7331
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6650
7332
|
});
|
|
6651
7333
|
import { createHash as createHash2 } from "crypto";
|
|
6652
|
-
import { readFileSync as
|
|
7334
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6653
7335
|
function sha256OfText(text) {
|
|
6654
7336
|
return createHash2("sha256").update(text).digest("hex");
|
|
6655
7337
|
}
|
|
6656
7338
|
function sha256OfPath(path) {
|
|
6657
|
-
return createHash2("sha256").update(
|
|
7339
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6658
7340
|
}
|
|
6659
7341
|
function collectBundledCatalog() {
|
|
6660
7342
|
const out = [];
|
|
@@ -6670,9 +7352,8 @@ function collectBundledCatalog() {
|
|
|
6670
7352
|
}
|
|
6671
7353
|
return out;
|
|
6672
7354
|
}
|
|
6673
|
-
async function fetchJson(base, path, opts) {
|
|
7355
|
+
async function fetchJson(base, path, apiKey, opts) {
|
|
6674
7356
|
const headers = { accept: "application/json" };
|
|
6675
|
-
const apiKey = resolveApiKey();
|
|
6676
7357
|
if (apiKey)
|
|
6677
7358
|
headers["x-api-key"] = apiKey;
|
|
6678
7359
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -6681,7 +7362,7 @@ async function fetchJson(base, path, opts) {
|
|
|
6681
7362
|
signal: AbortSignal.timeout(15000)
|
|
6682
7363
|
});
|
|
6683
7364
|
if (res.status === 401) {
|
|
6684
|
-
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)");
|
|
6685
7366
|
}
|
|
6686
7367
|
if (!res.ok) {
|
|
6687
7368
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
@@ -6692,9 +7373,9 @@ async function fetchJson(base, path, opts) {
|
|
|
6692
7373
|
}
|
|
6693
7374
|
return body;
|
|
6694
7375
|
}
|
|
6695
|
-
async function fetchRemoteState(apiUrl) {
|
|
6696
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6697
|
-
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);
|
|
6698
7379
|
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6699
7380
|
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6700
7381
|
}
|
|
@@ -6737,20 +7418,25 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6737
7418
|
return diff;
|
|
6738
7419
|
}
|
|
6739
7420
|
async function planSync(options = {}) {
|
|
6740
|
-
const
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
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) };
|
|
6744
7428
|
}
|
|
6745
7429
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6746
7430
|
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6747
7431
|
}
|
|
6748
7432
|
async function syncHooks(options = {}) {
|
|
6749
|
-
const
|
|
6750
|
-
|
|
7433
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
7434
|
+
credentials: options.credentials
|
|
7435
|
+
});
|
|
7436
|
+
const plan = await planSync(options);
|
|
6751
7437
|
if (options.dryRun)
|
|
6752
7438
|
return { ...plan, dryRun: true };
|
|
6753
|
-
if (!
|
|
7439
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
6754
7440
|
const db = getDb();
|
|
6755
7441
|
for (const entry of collectBundledCatalog()) {
|
|
6756
7442
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
@@ -6765,19 +7451,20 @@ async function syncHooks(options = {}) {
|
|
|
6765
7451
|
}
|
|
6766
7452
|
return plan;
|
|
6767
7453
|
}
|
|
6768
|
-
const {
|
|
7454
|
+
const { origin, apiKey } = transport.authority;
|
|
7455
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
6769
7456
|
const diff = computeDiff(catalog, remoteLock);
|
|
6770
|
-
const staged = await stageSyncArtifacts(
|
|
6771
|
-
await commitSyncArtifacts(staged,
|
|
6772
|
-
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 };
|
|
6773
7460
|
}
|
|
6774
|
-
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
7461
|
+
async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
6775
7462
|
const staged = [];
|
|
6776
7463
|
for (const name of names) {
|
|
6777
7464
|
const entry = remoteLock.hooks[name];
|
|
6778
7465
|
if (!entry)
|
|
6779
7466
|
continue;
|
|
6780
|
-
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);
|
|
6781
7468
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6782
7469
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6783
7470
|
}
|
|
@@ -6844,8 +7531,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
6844
7531
|
throw err;
|
|
6845
7532
|
}
|
|
6846
7533
|
}
|
|
6847
|
-
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6848
|
-
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);
|
|
6849
7536
|
const entry = remoteLock.hooks?.[name];
|
|
6850
7537
|
if (!entry) {
|
|
6851
7538
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
@@ -6854,7 +7541,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6854
7541
|
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
6855
7542
|
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
6856
7543
|
}
|
|
6857
|
-
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" });
|
|
6858
7545
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6859
7546
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6860
7547
|
}
|
|
@@ -6907,7 +7594,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6907
7594
|
}
|
|
6908
7595
|
var init_sync = __esm(() => {
|
|
6909
7596
|
init_registry();
|
|
6910
|
-
|
|
7597
|
+
init_transport2();
|
|
6911
7598
|
init_db();
|
|
6912
7599
|
init_store();
|
|
6913
7600
|
init_manifest();
|
|
@@ -7547,7 +8234,7 @@ var init_openapi = __esm(() => {
|
|
|
7547
8234
|
apiKey: {
|
|
7548
8235
|
type: "http",
|
|
7549
8236
|
scheme: "bearer",
|
|
7550
|
-
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."
|
|
7551
8238
|
}
|
|
7552
8239
|
}
|
|
7553
8240
|
}
|
|
@@ -7558,18 +8245,19 @@ var init_openapi = __esm(() => {
|
|
|
7558
8245
|
var exports_serve = {};
|
|
7559
8246
|
__export(exports_serve, {
|
|
7560
8247
|
startServeServer: () => startServeServer,
|
|
8248
|
+
resolveServeOptions: () => resolveServeOptions,
|
|
7561
8249
|
handleServeRequest: () => handleServeRequest,
|
|
7562
8250
|
SERVE_SERVICE_NAME: () => SERVE_SERVICE_NAME,
|
|
7563
8251
|
SERVE_HOST: () => SERVE_HOST,
|
|
7564
8252
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
7565
8253
|
});
|
|
7566
|
-
import { readFileSync as
|
|
7567
|
-
import { dirname as dirname6, join as
|
|
8254
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8255
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
7568
8256
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7569
8257
|
function packageVersion() {
|
|
7570
8258
|
try {
|
|
7571
|
-
const pkgPath =
|
|
7572
|
-
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";
|
|
7573
8261
|
} catch {
|
|
7574
8262
|
return "0.0.0";
|
|
7575
8263
|
}
|
|
@@ -7717,15 +8405,20 @@ function handleServeRequest(req, apiKey) {
|
|
|
7717
8405
|
}
|
|
7718
8406
|
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
7719
8407
|
}
|
|
8408
|
+
function resolveServeOptions(options) {
|
|
8409
|
+
const envPort = process.env.PORT?.trim();
|
|
8410
|
+
const envHost = process.env.HOST?.trim();
|
|
8411
|
+
const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
|
|
8412
|
+
const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
|
|
8413
|
+
return { port, host };
|
|
8414
|
+
}
|
|
7720
8415
|
function startServeServer(options) {
|
|
7721
|
-
const port = options
|
|
7722
|
-
const host = options.host ?? SERVE_HOST;
|
|
7723
|
-
const apiKey = resolveApiKey();
|
|
8416
|
+
const { port, host } = resolveServeOptions(options);
|
|
7724
8417
|
const server = Bun.serve({
|
|
7725
8418
|
hostname: host,
|
|
7726
8419
|
port,
|
|
7727
8420
|
async fetch(req) {
|
|
7728
|
-
return handleServeRequest(req,
|
|
8421
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
7729
8422
|
}
|
|
7730
8423
|
});
|
|
7731
8424
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
@@ -7737,7 +8430,7 @@ var init_serve = __esm(() => {
|
|
|
7737
8430
|
init_manifest();
|
|
7738
8431
|
init_store();
|
|
7739
8432
|
init_resolve();
|
|
7740
|
-
|
|
8433
|
+
init_transport2();
|
|
7741
8434
|
init_openapi();
|
|
7742
8435
|
init_semver();
|
|
7743
8436
|
if (false) {}
|
|
@@ -7839,17 +8532,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
7839
8532
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7840
8533
|
import { z as z2 } from "zod";
|
|
7841
8534
|
import { createServer } from "http";
|
|
7842
|
-
import { existsSync as
|
|
7843
|
-
import { join as
|
|
8535
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
8536
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
7844
8537
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
7845
8538
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
7846
8539
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7847
8540
|
const resolved = resolveHook2(name);
|
|
7848
8541
|
const hookScript = resolveScriptPath2(name);
|
|
7849
|
-
if (!resolved || !hookScript || !
|
|
8542
|
+
if (!resolved || !hookScript || !existsSync14(hookScript)) {
|
|
7850
8543
|
throw new Error(`Hook '${name}' not found`);
|
|
7851
8544
|
}
|
|
7852
|
-
const content =
|
|
8545
|
+
const content = readFileSync10(hookScript);
|
|
7853
8546
|
const sha = sha256Of(content);
|
|
7854
8547
|
const check = checkScriptHash(name, sha);
|
|
7855
8548
|
if (!check.ok) {
|
|
@@ -8054,7 +8747,7 @@ function createHooksServer() {
|
|
|
8054
8747
|
const settingsPath = getSettingsPath(scope);
|
|
8055
8748
|
const issues = [];
|
|
8056
8749
|
const healthy = [];
|
|
8057
|
-
const settingsExist =
|
|
8750
|
+
const settingsExist = existsSync14(settingsPath);
|
|
8058
8751
|
if (!settingsExist) {
|
|
8059
8752
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
8060
8753
|
}
|
|
@@ -8064,7 +8757,7 @@ function createHooksServer() {
|
|
|
8064
8757
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
8065
8758
|
let hookHealthy = true;
|
|
8066
8759
|
if (custom) {
|
|
8067
|
-
if (!
|
|
8760
|
+
if (!existsSync14(custom.scriptPath)) {
|
|
8068
8761
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
8069
8762
|
hookHealthy = false;
|
|
8070
8763
|
}
|
|
@@ -8074,14 +8767,14 @@ function createHooksServer() {
|
|
|
8074
8767
|
continue;
|
|
8075
8768
|
}
|
|
8076
8769
|
const hookDir = getHookPath(name);
|
|
8077
|
-
if (!
|
|
8770
|
+
if (!existsSync14(join16(hookDir, "src", "hook.ts"))) {
|
|
8078
8771
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
8079
8772
|
hookHealthy = false;
|
|
8080
8773
|
}
|
|
8081
8774
|
}
|
|
8082
8775
|
if (meta && settingsExist) {
|
|
8083
8776
|
try {
|
|
8084
|
-
const settings = JSON.parse(
|
|
8777
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8085
8778
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
8086
8779
|
const eventName = meta.event.split(":")[0];
|
|
8087
8780
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -8111,10 +8804,10 @@ function createHooksServer() {
|
|
|
8111
8804
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
8112
8805
|
}
|
|
8113
8806
|
const hookPath = getHookPath(name);
|
|
8114
|
-
const readmePath =
|
|
8807
|
+
const readmePath = join16(hookPath, "README.md");
|
|
8115
8808
|
let readme = "";
|
|
8116
|
-
if (
|
|
8117
|
-
readme =
|
|
8809
|
+
if (existsSync14(readmePath)) {
|
|
8810
|
+
readme = readFileSync10(readmePath, "utf-8");
|
|
8118
8811
|
}
|
|
8119
8812
|
if (verbose) {
|
|
8120
8813
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -8297,7 +8990,7 @@ function createHooksServer() {
|
|
|
8297
8990
|
const ctx = {
|
|
8298
8991
|
scope,
|
|
8299
8992
|
settings_path: settingsPath,
|
|
8300
|
-
settings_exists:
|
|
8993
|
+
settings_exists: existsSync14(settingsPath),
|
|
8301
8994
|
registered_hooks: hooks,
|
|
8302
8995
|
hook_count: hooks.length,
|
|
8303
8996
|
healthy,
|
|
@@ -8443,8 +9136,8 @@ function createHooksServer() {
|
|
|
8443
9136
|
const settingsPath = getSettingsPath(scope);
|
|
8444
9137
|
let settings = {};
|
|
8445
9138
|
try {
|
|
8446
|
-
if (
|
|
8447
|
-
settings = JSON.parse(
|
|
9139
|
+
if (existsSync14(settingsPath))
|
|
9140
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8448
9141
|
} catch {}
|
|
8449
9142
|
if (!settings.hooks)
|
|
8450
9143
|
settings.hooks = {};
|
|
@@ -8452,10 +9145,10 @@ function createHooksServer() {
|
|
|
8452
9145
|
if (!disabled.includes(name))
|
|
8453
9146
|
disabled.push(name);
|
|
8454
9147
|
settings.hooks.__disabled = disabled;
|
|
8455
|
-
const { writeFileSync:
|
|
9148
|
+
const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
|
|
8456
9149
|
const { dirname: dirname8 } = await import("path");
|
|
8457
|
-
|
|
8458
|
-
|
|
9150
|
+
mkdirSync7(dirname8(settingsPath), { recursive: true });
|
|
9151
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
8459
9152
|
`);
|
|
8460
9153
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
8461
9154
|
});
|
|
@@ -8466,15 +9159,15 @@ function createHooksServer() {
|
|
|
8466
9159
|
const settingsPath = getSettingsPath(scope);
|
|
8467
9160
|
let settings = {};
|
|
8468
9161
|
try {
|
|
8469
|
-
if (
|
|
8470
|
-
settings = JSON.parse(
|
|
9162
|
+
if (existsSync14(settingsPath))
|
|
9163
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8471
9164
|
} catch {}
|
|
8472
9165
|
if (settings.hooks?.__disabled) {
|
|
8473
9166
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
8474
9167
|
if (settings.hooks.__disabled.length === 0)
|
|
8475
9168
|
delete settings.hooks.__disabled;
|
|
8476
|
-
const { writeFileSync:
|
|
8477
|
-
|
|
9169
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
9170
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
8478
9171
|
`);
|
|
8479
9172
|
}
|
|
8480
9173
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
@@ -8831,9 +9524,9 @@ var init_server = __esm(() => {
|
|
|
8831
9524
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
8832
9525
|
try {
|
|
8833
9526
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
8834
|
-
const p =
|
|
8835
|
-
if (
|
|
8836
|
-
pkg = JSON.parse(
|
|
9527
|
+
const p = join16(__dirname3, rel);
|
|
9528
|
+
if (existsSync14(p)) {
|
|
9529
|
+
pkg = JSON.parse(readFileSync10(p, "utf-8"));
|
|
8837
9530
|
break;
|
|
8838
9531
|
}
|
|
8839
9532
|
}
|
|
@@ -8904,9 +9597,13 @@ var init_http = () => {};
|
|
|
8904
9597
|
// ../events/dist/commander.js
|
|
8905
9598
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8906
9599
|
import { Buffer as Buffer2 } from "buffer";
|
|
9600
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9601
|
+
import { join as join2 } from "path";
|
|
8907
9602
|
import { existsSync } from "fs";
|
|
8908
9603
|
import { homedir } from "os";
|
|
8909
|
-
import { join } from "path";
|
|
9604
|
+
import { join, resolve } from "path";
|
|
9605
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
9606
|
+
import { join as pathsResolverJoin } from "path";
|
|
8910
9607
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
8911
9608
|
import { lookup as dnsLookup } from "dns/promises";
|
|
8912
9609
|
import { isIP } from "net";
|
|
@@ -9010,13 +9707,103 @@ function channelMatchesEvent(channel, event) {
|
|
|
9010
9707
|
return true;
|
|
9011
9708
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
9012
9709
|
}
|
|
9710
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
9711
|
+
config: "HASNA_CONFIG_HOME",
|
|
9712
|
+
data: "HASNA_DATA_HOME",
|
|
9713
|
+
state: "HASNA_STATE_HOME",
|
|
9714
|
+
cache: "HASNA_CACHE_HOME"
|
|
9715
|
+
};
|
|
9716
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9717
|
+
function pathsResolverAssertApp(app) {
|
|
9718
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
9719
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
9720
|
+
}
|
|
9721
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
9722
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
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
|
+
}
|
|
9729
|
+
}
|
|
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)
|
|
9735
|
+
return override;
|
|
9736
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
9737
|
+
const platform = options.platform ?? process.platform;
|
|
9738
|
+
if (platform === "darwin") {
|
|
9739
|
+
switch (kind) {
|
|
9740
|
+
case "config":
|
|
9741
|
+
case "data":
|
|
9742
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
9743
|
+
case "cache":
|
|
9744
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
9745
|
+
case "state":
|
|
9746
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
9747
|
+
}
|
|
9748
|
+
}
|
|
9749
|
+
switch (kind) {
|
|
9750
|
+
case "config":
|
|
9751
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
9752
|
+
case "data":
|
|
9753
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
9754
|
+
case "state":
|
|
9755
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
9756
|
+
case "cache":
|
|
9757
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
9758
|
+
}
|
|
9759
|
+
}
|
|
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);
|
|
9764
|
+
}
|
|
9765
|
+
function dataDir(options) {
|
|
9766
|
+
return pathsResolverResolve("data", options);
|
|
9767
|
+
}
|
|
9013
9768
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
9014
9769
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
9770
|
+
var EVENTS_STORE_SENTINEL_FILE = "events.json";
|
|
9771
|
+
function effectiveHome() {
|
|
9772
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
9773
|
+
}
|
|
9774
|
+
function legacyHomeDir() {
|
|
9775
|
+
return join(effectiveHome(), ".hasna", "events");
|
|
9776
|
+
}
|
|
9777
|
+
function resolverHome() {
|
|
9778
|
+
return dataDir({ app: "events", home: effectiveHome() || undefined });
|
|
9779
|
+
}
|
|
9780
|
+
function adoptResolverHome(resolved, env = process.env) {
|
|
9781
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
9782
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
9783
|
+
return true;
|
|
9784
|
+
return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
9785
|
+
}
|
|
9786
|
+
function exactEventsHome() {
|
|
9787
|
+
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
9788
|
+
if (dir && dir.trim())
|
|
9789
|
+
return dir.trim();
|
|
9790
|
+
const home = process.env[HASNA_EVENTS_HOME_ENV];
|
|
9791
|
+
if (home && home.trim())
|
|
9792
|
+
return home.trim();
|
|
9793
|
+
return;
|
|
9794
|
+
}
|
|
9795
|
+
function getEventsHome() {
|
|
9796
|
+
const exact = exactEventsHome();
|
|
9797
|
+
if (exact)
|
|
9798
|
+
return resolve(exact);
|
|
9799
|
+
const resolved = resolverHome();
|
|
9800
|
+
return adoptResolverHome(resolved) ? resolve(resolved) : resolve(legacyHomeDir());
|
|
9801
|
+
}
|
|
9015
9802
|
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
9016
9803
|
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
9017
9804
|
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
9018
9805
|
function getEventsDataDir(override) {
|
|
9019
|
-
return override ||
|
|
9806
|
+
return override || getEventsHome();
|
|
9020
9807
|
}
|
|
9021
9808
|
function getActiveEventsDirEnv() {
|
|
9022
9809
|
if (process.env[HASNA_EVENTS_DIR_ENV])
|
|
@@ -9032,12 +9819,12 @@ class JsonEventsStore {
|
|
|
9032
9819
|
channelsPath;
|
|
9033
9820
|
eventsPath;
|
|
9034
9821
|
deliveriesPath;
|
|
9035
|
-
constructor(
|
|
9036
|
-
this.dataDir =
|
|
9037
|
-
this.runtime = localJsonRuntime(
|
|
9038
|
-
this.channelsPath =
|
|
9039
|
-
this.eventsPath =
|
|
9040
|
-
this.deliveriesPath =
|
|
9822
|
+
constructor(dataDir2 = getEventsDataDir()) {
|
|
9823
|
+
this.dataDir = dataDir2;
|
|
9824
|
+
this.runtime = localJsonRuntime(dataDir2);
|
|
9825
|
+
this.channelsPath = join2(dataDir2, "channels.json");
|
|
9826
|
+
this.eventsPath = join2(dataDir2, "events.json");
|
|
9827
|
+
this.deliveriesPath = join2(dataDir2, "deliveries.json");
|
|
9041
9828
|
}
|
|
9042
9829
|
async init() {
|
|
9043
9830
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -9154,7 +9941,7 @@ class JsonEventsStore {
|
|
|
9154
9941
|
};
|
|
9155
9942
|
}
|
|
9156
9943
|
async ensureArrayFile(path) {
|
|
9157
|
-
if (!
|
|
9944
|
+
if (!existsSync2(path)) {
|
|
9158
9945
|
await writeFile(path, `[]
|
|
9159
9946
|
`, { encoding: "utf-8", mode: 384 });
|
|
9160
9947
|
}
|
|
@@ -9184,7 +9971,7 @@ class JsonEventsStore {
|
|
|
9184
9971
|
});
|
|
9185
9972
|
}
|
|
9186
9973
|
}
|
|
9187
|
-
function localJsonRuntime(
|
|
9974
|
+
function localJsonRuntime(dataDir2 = getEventsDataDir()) {
|
|
9188
9975
|
return {
|
|
9189
9976
|
mode: "local-files",
|
|
9190
9977
|
name: "json-events-store",
|
|
@@ -9197,7 +9984,7 @@ function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
|
9197
9984
|
durable: true,
|
|
9198
9985
|
idempotency: "best-effort-local",
|
|
9199
9986
|
replayCursors: true,
|
|
9200
|
-
description: `Local JSON files in ${
|
|
9987
|
+
description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
9201
9988
|
};
|
|
9202
9989
|
}
|
|
9203
9990
|
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
@@ -9261,8 +10048,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
|
|
|
9261
10048
|
function findEventByIdentity(events, identity) {
|
|
9262
10049
|
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
9263
10050
|
}
|
|
9264
|
-
async function getEventsStatus(
|
|
9265
|
-
const store = new JsonEventsStore(
|
|
10051
|
+
async function getEventsStatus(dataDir2) {
|
|
10052
|
+
const store = new JsonEventsStore(dataDir2);
|
|
9266
10053
|
await store.init();
|
|
9267
10054
|
const [channels, events, deliveries] = await Promise.all([
|
|
9268
10055
|
store.listChannels(),
|
|
@@ -9304,9 +10091,9 @@ async function getEventsStatus(dataDir) {
|
|
|
9304
10091
|
}
|
|
9305
10092
|
};
|
|
9306
10093
|
}
|
|
9307
|
-
function statusFile(
|
|
9308
|
-
const path =
|
|
9309
|
-
return { path, exists:
|
|
10094
|
+
function statusFile(dataDir2, fileName, records) {
|
|
10095
|
+
const path = join2(dataDir2, fileName);
|
|
10096
|
+
return { path, exists: existsSync2(path), records };
|
|
9310
10097
|
}
|
|
9311
10098
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
9312
10099
|
function buildSignatureBase(timestamp, body) {
|
|
@@ -9645,7 +10432,7 @@ async function pinnedNativeRequest(target, addresses, method, headers, body, sig
|
|
|
9645
10432
|
callback(null, entries);
|
|
9646
10433
|
}
|
|
9647
10434
|
};
|
|
9648
|
-
return new Promise((
|
|
10435
|
+
return new Promise((resolve2, reject) => {
|
|
9649
10436
|
const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
|
|
9650
10437
|
const onAbort = () => {
|
|
9651
10438
|
const error = new Error("The operation was aborted.");
|
|
@@ -9672,7 +10459,7 @@ async function pinnedNativeRequest(target, addresses, method, headers, body, sig
|
|
|
9672
10459
|
else if (Array.isArray(value))
|
|
9673
10460
|
headersRecord[name] = value.join(", ");
|
|
9674
10461
|
}
|
|
9675
|
-
|
|
10462
|
+
resolve2(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
|
|
9676
10463
|
});
|
|
9677
10464
|
}
|
|
9678
10465
|
});
|
|
@@ -9769,7 +10556,7 @@ async function dispatchCommand(event, channel) {
|
|
|
9769
10556
|
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
9770
10557
|
HASNA_EVENT_JSON: eventJson
|
|
9771
10558
|
};
|
|
9772
|
-
return new Promise((
|
|
10559
|
+
return new Promise((resolve2) => {
|
|
9773
10560
|
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
9774
10561
|
cwd: channel.command.cwd,
|
|
9775
10562
|
env,
|
|
@@ -9787,7 +10574,7 @@ async function dispatchCommand(event, channel) {
|
|
|
9787
10574
|
});
|
|
9788
10575
|
child.on("error", (error) => {
|
|
9789
10576
|
clearTimeout(timeout);
|
|
9790
|
-
|
|
10577
|
+
resolve2({
|
|
9791
10578
|
attempt: 1,
|
|
9792
10579
|
status: "failed",
|
|
9793
10580
|
startedAt,
|
|
@@ -9800,7 +10587,7 @@ async function dispatchCommand(event, channel) {
|
|
|
9800
10587
|
child.on("close", (code, signal) => {
|
|
9801
10588
|
clearTimeout(timeout);
|
|
9802
10589
|
const success = code === 0;
|
|
9803
|
-
|
|
10590
|
+
resolve2({
|
|
9804
10591
|
attempt: 1,
|
|
9805
10592
|
status: success ? "success" : "failed",
|
|
9806
10593
|
startedAt,
|
|
@@ -10239,6 +11026,13 @@ function parseMatcherExpression(value, label) {
|
|
|
10239
11026
|
negated: false
|
|
10240
11027
|
};
|
|
10241
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
|
+
}
|
|
10242
11036
|
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
10243
11037
|
function parseJsonObject(value, fallback) {
|
|
10244
11038
|
if (!value)
|
|
@@ -10264,7 +11058,7 @@ function parseHeaders(values) {
|
|
|
10264
11058
|
function createClient(options) {
|
|
10265
11059
|
if (options.createClient)
|
|
10266
11060
|
return options.createClient();
|
|
10267
|
-
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
11061
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir), webhookTargetPolicy: webhookTargetPolicyFromEnv() });
|
|
10268
11062
|
}
|
|
10269
11063
|
function print(value, json, text) {
|
|
10270
11064
|
if (json)
|
|
@@ -10345,6 +11139,8 @@ function registerChannelCommands(program, options) {
|
|
|
10345
11139
|
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
10346
11140
|
}, { honorFilters: actionOptions.honorFilters });
|
|
10347
11141
|
print(result, json, `${result.status}: ${result.channelId}`);
|
|
11142
|
+
if (result.status === "failed")
|
|
11143
|
+
process.exitCode = 1;
|
|
10348
11144
|
} catch (error) {
|
|
10349
11145
|
fail(error, json);
|
|
10350
11146
|
}
|
|
@@ -10455,8 +11251,8 @@ var {
|
|
|
10455
11251
|
|
|
10456
11252
|
// src/cli/index.tsx
|
|
10457
11253
|
import chalk2 from "chalk";
|
|
10458
|
-
import { existsSync as
|
|
10459
|
-
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";
|
|
10460
11256
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
10461
11257
|
|
|
10462
11258
|
// src/cli/components/App.tsx
|
|
@@ -11637,10 +12433,14 @@ init_manifest();
|
|
|
11637
12433
|
init_resolve();
|
|
11638
12434
|
init_store();
|
|
11639
12435
|
init_semver();
|
|
12436
|
+
init_app_home();
|
|
12437
|
+
init_config();
|
|
12438
|
+
init_local_opt_in();
|
|
12439
|
+
init_transport2();
|
|
11640
12440
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
11641
12441
|
var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
|
|
11642
|
-
var pkgPath =
|
|
11643
|
-
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"));
|
|
11644
12444
|
var program2 = new Command;
|
|
11645
12445
|
function resolveScope(options) {
|
|
11646
12446
|
if (options.project)
|
|
@@ -11674,7 +12474,7 @@ function projectLogRows2(rows) {
|
|
|
11674
12474
|
function readToken(tokenFile) {
|
|
11675
12475
|
if (tokenFile) {
|
|
11676
12476
|
try {
|
|
11677
|
-
return
|
|
12477
|
+
return readFileSync11(tokenFile, "utf-8").trim();
|
|
11678
12478
|
} catch {
|
|
11679
12479
|
return;
|
|
11680
12480
|
}
|
|
@@ -11739,7 +12539,6 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
|
|
|
11739
12539
|
});
|
|
11740
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) => {
|
|
11741
12541
|
if (options.cloudflare) {
|
|
11742
|
-
const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11743
12542
|
if (!options.apiUrl) {
|
|
11744
12543
|
const message = "--cloudflare requires --api-url <url>";
|
|
11745
12544
|
if (options.json)
|
|
@@ -11749,22 +12548,39 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
11749
12548
|
return;
|
|
11750
12549
|
}
|
|
11751
12550
|
const apiKeyRef = options.apiKey ?? "hasna/hooks/live/api-key";
|
|
11752
|
-
const
|
|
11753
|
-
const path = writeConfig2(config);
|
|
12551
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
11754
12552
|
if (options.json) {
|
|
11755
|
-
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
|
+
}));
|
|
11756
12563
|
return;
|
|
11757
12564
|
}
|
|
11758
12565
|
console.log(chalk2.green(`
|
|
11759
|
-
\u2713 Remote registry
|
|
12566
|
+
\u2713 Remote registry configuration for ${apiUrl}
|
|
11760
12567
|
`));
|
|
11761
|
-
console.log(` ${chalk2.dim("API URL:")} ${
|
|
11762
|
-
console.log(` ${chalk2.dim("API key ref:")} ${
|
|
11763
|
-
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`);
|
|
11764
12580
|
console.log();
|
|
11765
|
-
console.log(chalk2.dim("
|
|
11766
|
-
console.log(
|
|
11767
|
-
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)"));
|
|
11768
12584
|
console.log();
|
|
11769
12585
|
return;
|
|
11770
12586
|
}
|
|
@@ -11791,7 +12607,7 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
11791
12607
|
if (profile.name) {
|
|
11792
12608
|
console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
|
|
11793
12609
|
}
|
|
11794
|
-
console.log(` ${chalk2.dim("Profile:")}
|
|
12610
|
+
console.log(` ${chalk2.dim("Profile:")} ${join17(getProfilesDir(), `${profile.agent_id}.json`)}`);
|
|
11795
12611
|
console.log();
|
|
11796
12612
|
console.log(chalk2.dim(" Install hooks with this profile:"));
|
|
11797
12613
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
@@ -11807,11 +12623,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
11807
12623
|
process.exit(1);
|
|
11808
12624
|
}
|
|
11809
12625
|
const hookScript = resolveScriptPath2(hook);
|
|
11810
|
-
if (!hookScript || !
|
|
12626
|
+
if (!hookScript || !existsSync15(hookScript)) {
|
|
11811
12627
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
11812
12628
|
process.exit(1);
|
|
11813
12629
|
}
|
|
11814
|
-
const content =
|
|
12630
|
+
const content = readFileSync11(hookScript);
|
|
11815
12631
|
const sha = sha256Of2(content);
|
|
11816
12632
|
const check = checkScriptHash2(hook, sha);
|
|
11817
12633
|
if (!check.ok) {
|
|
@@ -11952,7 +12768,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
11952
12768
|
}
|
|
11953
12769
|
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
11954
12770
|
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
11955
|
-
const {
|
|
12771
|
+
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
11956
12772
|
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
11957
12773
|
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
11958
12774
|
function parseNameVersion(arg) {
|
|
@@ -12021,7 +12837,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
12021
12837
|
const { customHookDir: customHookDir2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12022
12838
|
const { removeHookFromStore: removeHookFromStore2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12023
12839
|
try {
|
|
12024
|
-
|
|
12840
|
+
rmSync4(customHookDir2(custom.name), { recursive: true, force: true });
|
|
12025
12841
|
} catch {}
|
|
12026
12842
|
removeHookFromStore2(custom.name);
|
|
12027
12843
|
throw pinError;
|
|
@@ -12045,11 +12861,13 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
12045
12861
|
const installedPinned = [];
|
|
12046
12862
|
for (const { arg, pinned } of pinnedRequests) {
|
|
12047
12863
|
try {
|
|
12048
|
-
const
|
|
12049
|
-
if (!
|
|
12050
|
-
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;
|
|
12051
12869
|
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12052
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version,
|
|
12870
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
12053
12871
|
installedPinned.push(pinnedInstall.name);
|
|
12054
12872
|
if (options.json) {
|
|
12055
12873
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
@@ -12345,7 +13163,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
12345
13163
|
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12346
13164
|
const custom = readCustomManifest2(meta.name);
|
|
12347
13165
|
const source = custom ? "custom" : "bundled";
|
|
12348
|
-
const sourceNote = custom ?
|
|
13166
|
+
const sourceNote = custom ? `custom (${getCustomHooksDir()}/) overrides the bundled registry` : "bundled registry";
|
|
12349
13167
|
if (options.json) {
|
|
12350
13168
|
console.log(JSON.stringify({ ...meta, source, source_note: sourceNote, global: globalInstalled, project: projectInstalled }));
|
|
12351
13169
|
return;
|
|
@@ -12379,7 +13197,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12379
13197
|
const settingsPath = getSettingsPath(scope);
|
|
12380
13198
|
const issues = [];
|
|
12381
13199
|
const healthy = [];
|
|
12382
|
-
const settingsExist =
|
|
13200
|
+
const settingsExist = existsSync15(settingsPath);
|
|
12383
13201
|
if (!settingsExist) {
|
|
12384
13202
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
12385
13203
|
}
|
|
@@ -12387,7 +13205,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12387
13205
|
let wiringCount = 0;
|
|
12388
13206
|
if (settingsExist) {
|
|
12389
13207
|
try {
|
|
12390
|
-
const settings = JSON.parse(
|
|
13208
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
12391
13209
|
wiringCount = countSettingsWiring(settings);
|
|
12392
13210
|
} catch {}
|
|
12393
13211
|
}
|
|
@@ -12396,7 +13214,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12396
13214
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
12397
13215
|
let hookHealthy = true;
|
|
12398
13216
|
if (custom) {
|
|
12399
|
-
if (!
|
|
13217
|
+
if (!existsSync15(custom.scriptPath)) {
|
|
12400
13218
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
12401
13219
|
hookHealthy = false;
|
|
12402
13220
|
}
|
|
@@ -12407,15 +13225,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
12407
13225
|
continue;
|
|
12408
13226
|
}
|
|
12409
13227
|
const hookDir = getHookPath(name);
|
|
12410
|
-
const hookScript =
|
|
12411
|
-
if (!
|
|
13228
|
+
const hookScript = join17(hookDir, "src", "hook.ts");
|
|
13229
|
+
if (!existsSync15(hookScript)) {
|
|
12412
13230
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
12413
13231
|
hookHealthy = false;
|
|
12414
13232
|
}
|
|
12415
13233
|
}
|
|
12416
13234
|
if (meta && settingsExist) {
|
|
12417
13235
|
try {
|
|
12418
|
-
const settings = JSON.parse(
|
|
13236
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
12419
13237
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
12420
13238
|
const eventName = meta.event.split(":")[0];
|
|
12421
13239
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -12482,7 +13300,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
12482
13300
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
12483
13301
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12484
13302
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12485
|
-
const {
|
|
13303
|
+
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12486
13304
|
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
12487
13305
|
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12488
13306
|
function parseNameVersion(arg) {
|
|
@@ -12504,11 +13322,13 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
12504
13322
|
const pinned = parseNameVersion(arg);
|
|
12505
13323
|
if (pinned) {
|
|
12506
13324
|
try {
|
|
12507
|
-
const
|
|
12508
|
-
if (!
|
|
12509
|
-
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;
|
|
12510
13330
|
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12511
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version,
|
|
13331
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
12512
13332
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
12513
13333
|
if (!registered.success) {
|
|
12514
13334
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -12582,10 +13402,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
12582
13402
|
return;
|
|
12583
13403
|
}
|
|
12584
13404
|
const hookPath = getHookPath(hook);
|
|
12585
|
-
const readmePath =
|
|
13405
|
+
const readmePath = join17(hookPath, "README.md");
|
|
12586
13406
|
let readme = "";
|
|
12587
|
-
if (
|
|
12588
|
-
readme =
|
|
13407
|
+
if (existsSync15(readmePath)) {
|
|
13408
|
+
readme = readFileSync11(readmePath, "utf-8");
|
|
12589
13409
|
}
|
|
12590
13410
|
if (options.json) {
|
|
12591
13411
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -12769,8 +13589,8 @@ program2.command("profile-export").description("Export all agent profiles as JSO
|
|
|
12769
13589
|
const profiles = exportProfiles();
|
|
12770
13590
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
12771
13591
|
if (options.output) {
|
|
12772
|
-
const { writeFileSync:
|
|
12773
|
-
|
|
13592
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
13593
|
+
writeFileSync6(options.output, json2 + `
|
|
12774
13594
|
`);
|
|
12775
13595
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
12776
13596
|
} else {
|
|
@@ -12782,9 +13602,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
12782
13602
|
if (file === "-") {
|
|
12783
13603
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
12784
13604
|
} else {
|
|
12785
|
-
const { readFileSync:
|
|
13605
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
12786
13606
|
try {
|
|
12787
|
-
raw =
|
|
13607
|
+
raw = readFileSync12(file, "utf-8");
|
|
12788
13608
|
} catch {
|
|
12789
13609
|
if (options.json) {
|
|
12790
13610
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -12815,7 +13635,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
12815
13635
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
12816
13636
|
}
|
|
12817
13637
|
});
|
|
12818
|
-
var logCmd = program2.command("log").description(
|
|
13638
|
+
var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
12819
13639
|
logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
12820
13640
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12821
13641
|
const db = getDb2();
|
|
@@ -13064,14 +13884,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
13064
13884
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
13065
13885
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
13066
13886
|
});
|
|
13067
|
-
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) => {
|
|
13068
13888
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
13069
13889
|
startServeServer2({
|
|
13070
13890
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
13071
13891
|
host: options.host
|
|
13072
13892
|
});
|
|
13073
13893
|
});
|
|
13074
|
-
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) => {
|
|
13075
13895
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13076
13896
|
try {
|
|
13077
13897
|
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
@@ -13202,4 +14022,62 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
13202
14022
|
}
|
|
13203
14023
|
});
|
|
13204
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();
|
|
13205
14083
|
program2.parse();
|