@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.
Files changed (53) hide show
  1. package/README.md +32 -6
  2. package/bin/index.js +1573 -695
  3. package/bin/serve.js +713 -335
  4. package/dist/config.d.ts +25 -19
  5. package/dist/db/index.d.ts +3 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.js +1162 -473
  8. package/dist/lib/app-home.d.ts +74 -0
  9. package/dist/lib/installer.d.ts +0 -9
  10. package/dist/lib/local-opt-in.d.ts +80 -0
  11. package/dist/lib/profiles.d.ts +14 -1
  12. package/dist/lib/resolver-types.d.ts +45 -0
  13. package/dist/lib/sync.d.ts +13 -8
  14. package/dist/lib/transport.d.ts +87 -0
  15. package/dist/openapi.d.ts +1 -1
  16. package/dist/serve.d.ts +7 -0
  17. package/dist/storage.js +141 -27
  18. package/hooks/hook-affected-tests/package.json +1 -1
  19. package/hooks/hook-agent-rules-version-check/package.json +1 -1
  20. package/hooks/hook-agentmessages/package.json +1 -1
  21. package/hooks/hook-announce-start/package.json +1 -1
  22. package/hooks/hook-announce-stop/package.json +1 -1
  23. package/hooks/hook-autoformat/package.json +1 -1
  24. package/hooks/hook-branchprotect/package.json +1 -1
  25. package/hooks/hook-checkbugs/package.json +1 -1
  26. package/hooks/hook-checkdocs/package.json +1 -1
  27. package/hooks/hook-checkfiles/package.json +1 -1
  28. package/hooks/hook-checklint/package.json +1 -1
  29. package/hooks/hook-checkpoint/package.json +1 -1
  30. package/hooks/hook-checksecurity/package.json +1 -1
  31. package/hooks/hook-checktasks/package.json +1 -1
  32. package/hooks/hook-checktests/package.json +1 -1
  33. package/hooks/hook-conflict-detect/package.json +1 -1
  34. package/hooks/hook-contextrefresh/package.json +1 -1
  35. package/hooks/hook-desktopnotify/package.json +1 -1
  36. package/hooks/hook-dm-inject/package.json +1 -1
  37. package/hooks/hook-envsetup/package.json +1 -1
  38. package/hooks/hook-failure-to-task/package.json +1 -1
  39. package/hooks/hook-filelock/package.json +1 -1
  40. package/hooks/hook-fleet-blockers-gate/package.json +1 -1
  41. package/hooks/hook-fleet-catchup/package.json +1 -1
  42. package/hooks/hook-gitguard/package.json +1 -1
  43. package/hooks/hook-packageage/package.json +1 -1
  44. package/hooks/hook-permissionguard/package.json +1 -1
  45. package/hooks/hook-phonenotify/package.json +1 -1
  46. package/hooks/hook-precompact/package.json +1 -1
  47. package/hooks/hook-protectfiles/package.json +1 -1
  48. package/hooks/hook-stylescheck/package.json +1 -1
  49. package/hooks/hook-typecheck-gate/package.json +1 -1
  50. package/package.json +6 -9
  51. package/scripts/artifact-scan.ts +49 -0
  52. package/scripts/ensure-profiles-dir.mjs +99 -0
  53. 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: "Logs every tool call to SQLite (~/.hasna/hooks/hooks.db)",
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: "Logs every Bash command to SQLite (~/.hasna/hooks/hooks.db)",
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: "Detects tool failures and logs errors to SQLite (~/.hasna/hooks/hooks.db)",
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
- var exports_config = {};
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
- const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4363
- if (explicit)
4364
- return explicit;
4365
- return join3(homedir2(), ".hasna", "hooks");
4193
+ return getEffectiveDataRoot();
4366
4194
  }
4367
4195
  function getCustomHooksDir() {
4368
- return join3(getHooksDataDir(), "hooks");
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 join3(getHooksDataDir(), "hooks.lock");
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
- init_transport();
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 existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4438
- import { basename, dirname, isAbsolute, join as join4, normalize, resolve, sep } from "path";
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 = resolve(target);
4455
- const rootLexical = resolve(root);
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 (!existsSync3(cur)) {
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 join4(realpathSync(cur), ...tail);
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(join4(manifestDir, rel), manifestDir, manifest.script);
4505
- if (!existsSync3(resolved)) {
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: readFileSync3(resolved, "utf-8") };
4303
+ return { path: rel, content: readFileSync(resolved, "utf-8") };
4509
4304
  }
4510
4305
  function customHookDir(name) {
4511
- return join4(getCustomHooksDir(), shortManifestName(name));
4306
+ return join6(getCustomHooksDir(), shortManifestName(name));
4512
4307
  }
4513
4308
  function readCustomManifest(name) {
4514
4309
  const dir = customHookDir(name);
4515
- const manifestPath = join4(dir, "manifest.json");
4516
- if (!existsSync3(manifestPath))
4310
+ const manifestPath = join6(dir, "manifest.json");
4311
+ if (!existsSync5(manifestPath))
4517
4312
  return;
4518
4313
  try {
4519
- const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
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: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
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 (!existsSync3(dir))
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("/") ? join4(dir, scriptRelPath) : join4(dir, scriptRelPath);
4341
+ const scriptPath = scriptRelPath.includes("/") ? join6(dir, scriptRelPath) : join6(dir, scriptRelPath);
4547
4342
  assertContainedLexical(scriptPath, dir, scriptRelPath);
4548
- mkdirSync2(dir, { recursive: true });
4343
+ mkdirSync(dir, { recursive: true });
4549
4344
  assertContainedReal(dir, scriptPath, scriptRelPath);
4550
4345
  const scriptDir = dirname(scriptPath);
4551
4346
  if (scriptDir !== dir)
4552
- mkdirSync2(scriptDir, { recursive: true });
4553
- writeFileSync2(scriptPath, scriptContent, "utf-8");
4554
- writeFileSync2(join4(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
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 join5 } from "path";
4599
- import { existsSync as existsSync4 } from "fs";
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 = join5(moduleDir, "..", "..", "hooks");
4603
- if (existsSync4(join5(sourceLayout, "hook-gitguard")))
4397
+ const sourceLayout = join7(moduleDir, "..", "..", "hooks");
4398
+ if (existsSync6(join7(sourceLayout, "hook-gitguard")))
4604
4399
  return sourceLayout;
4605
- return join5(moduleDir, "..", "hooks");
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 = join5(bundledBase, short);
4617
- if (existsSync4(direct))
4411
+ const direct = join7(bundledBase, short);
4412
+ if (existsSync6(direct))
4618
4413
  return direct;
4619
- const prefixed = join5(bundledBase, `hook-${short}`);
4620
- return existsSync4(prefixed) ? prefixed : undefined;
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 = join5(dir, "src", "hook.ts");
4630
- return existsSync4(script) ? script : undefined;
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 existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
4880
- import { join as join6 } from "path";
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 = readFileSync4(filePath, "utf-8").split(`
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 = readFileSync4(filePath, "utf-8").split(`
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, homeDir2 = homedir3()) {
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 = join6(homeDir2, ".claude", "projects");
4961
- if (existsSync5(claudeProjectsDir)) {
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 = join6(claudeProjectsDir, dir);
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, join6(projectDir, file));
4765
+ total += importJsonlFile(db, join8(projectDir, file));
4971
4766
  }
4972
4767
  if (file === "errors.log") {
4973
- total += importErrorsLog(db, join6(projectDir, file));
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 existsSync6, mkdirSync as mkdirSync3, cpSync } from "fs";
5017
- import { join as join7 } from "path";
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 explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
5021
- if (explicit)
5022
- return explicit;
5023
- const newDir = join7(homedir4(), ".hasna", "hooks");
5024
- const oldDir = join7(homedir4(), ".hooks");
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 newDir;
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 dataDir = resolveDataDir();
5036
- return join7(dataDir, "hooks.db");
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 && !existsSync6(dir)) {
5041
- mkdirSync3(dir, { recursive: true });
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:" || !existsSync6(dbPath);
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 existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
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 (!existsSync7(path))
4964
+ if (!existsSync9(path))
5173
4965
  return { hooks: {} };
5174
4966
  let raw;
5175
4967
  try {
5176
- raw = readFileSync5(path, "utf-8");
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
- mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
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
- writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
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(readFileSync5(scriptPath));
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 existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
5315
- import { join as join8, dirname as dirname3 } from "path";
5316
- import { homedir as homedir5 } from "os";
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" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
5147
+ return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
5356
5148
  }
5357
- return target === "codewith" ? join8(homedir5(), dir, "config.toml") : join8(homedir5(), dir, "settings.json");
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 join8(HOOKS_DIR, normalizeHookName(shortName));
5156
+ return join10(HOOKS_DIR, normalizeHookName(shortName));
5365
5157
  }
5366
5158
  function hookExists(name) {
5367
- return existsSync8(getHookPath(name));
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 (existsSync8(path)) {
5373
- return JSON.parse(readFileSync6(path, "utf-8"));
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 (!existsSync8(dir)) {
5384
- mkdirSync5(dir, { recursive: true });
5175
+ if (!existsSync10(dir)) {
5176
+ mkdirSync4(dir, { recursive: true });
5385
5177
  }
5386
- writeFileSync4(path, JSON.stringify(settings, null, 2) + `
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 existsSync8(path) ? readFileSync6(path, "utf-8") : "";
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 (!existsSync8(dir))
5481
- mkdirSync5(dir, { recursive: true });
5482
- const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
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
- writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
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 (!existsSync8(path))
5574
+ if (!existsSync10(path))
5783
5575
  return false;
5784
5576
  try {
5785
- return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
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 (existsSync8(codewithPath)) {
5817
- const before = readFileSync6(codewithPath, "utf-8");
5622
+ if (existsSync10(codewithPath)) {
5623
+ const before = readFileSync4(codewithPath, "utf-8");
5818
5624
  const after = removeCodewithHookEntry(before, shortName);
5819
5625
  if (after.removed) {
5820
- writeFileSync4(codewithPath, after.text, "utf-8");
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
- rmSync(customHookDir(shortName), { recursive: true, force: true });
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 = existsSync8(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
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 projectEventRowForRead(row) {
5977
- const out = { ...row };
5978
- for (const field of ["tool_input", "error", "metadata"]) {
5979
- const value = out[field];
5980
- if (typeof value === "string")
5981
- out[field] = redactEventPayload(value);
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 out;
6405
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
5986
6406
  }
5987
- var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
5988
- var init_redact = __esm(() => {
5989
- 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;
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 newDir;
6411
+ return name;
6021
6412
  }
6022
- function ensureProfilesDir() {
6023
- if (!existsSync9(PROFILES_DIR)) {
6024
- mkdirSync6(PROFILES_DIR, { recursive: true });
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 profilePath(id) {
6028
- return join9(PROFILES_DIR, `${id}.json`);
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 shortUuid() {
6031
- return crypto.randomUUID().slice(0, 8);
6473
+ function isDeliberateLoopbackHttpAuthority(authority) {
6474
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
6032
6475
  }
6033
- function createProfile(input) {
6034
- ensureProfilesDir();
6035
- const id = shortUuid();
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
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
6048
- `);
6049
- return profile;
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 getProfile(id) {
6052
- const path = profilePath(id);
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
- if (!existsSync9(path))
6055
- return null;
6056
- return JSON.parse(readFileSync7(path, "utf-8"));
6057
- } catch {
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 listProfiles() {
6062
- if (!existsSync9(PROFILES_DIR))
6063
- return [];
6064
- try {
6065
- const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
6066
- const profiles = [];
6067
- for (const file of files) {
6068
- try {
6069
- const content = readFileSync7(join9(PROFILES_DIR, file), "utf-8");
6070
- profiles.push(JSON.parse(content));
6071
- } catch {}
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
- return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
6074
- } catch {
6075
- return [];
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
- function touchProfile(id) {
6079
- const profile = getProfile(id);
6080
- if (!profile)
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
- profile.last_seen_at = new Date().toISOString();
6083
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
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 exportProfiles() {
6087
- return listProfiles();
6728
+ function announceHooksLocalMode(reason = "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority", notice) {
6729
+ announceLocal(notice, reason);
6088
6730
  }
6089
- function importProfiles(profiles) {
6090
- ensureProfilesDir();
6091
- let imported = 0;
6092
- let skipped = 0;
6093
- for (const profile of profiles) {
6094
- if (!profile.agent_id || !profile.agent_type) {
6095
- skipped++;
6096
- continue;
6097
- }
6098
- const path = profilePath(profile.agent_id);
6099
- if (existsSync9(path)) {
6100
- skipped++;
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
- writeFileSync5(path, JSON.stringify(profile, null, 2) + `
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
- return { imported, skipped };
6746
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
6108
6747
  }
6109
- var PROFILES_DIR;
6110
- var init_profiles = __esm(() => {
6111
- PROFILES_DIR = resolveProfilesDir();
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 isAbsolute2 } from "path";
6212
- import { existsSync as existsSync10, statSync as statSync2 } from "fs";
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 === "" || !isAbsolute2(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 ((statSync2(entry).mode & 2) !== 0)
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" && existsSync10("/opt/homebrew/bin"))
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 rmSync3, writeFileSync as writeFileSync6 } from "fs";
6372
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join10 } from "path";
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 = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
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
- writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
6452
- proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
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
- rmSync3(tempPath, { force: true });
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 existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
6535
- import { join as join11 } from "path";
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 = join11(dir, "manifest.json");
6539
- if (!existsSync11(path))
7220
+ const path = join14(dir, "manifest.json");
7221
+ if (!existsSync13(path))
6540
7222
  return;
6541
7223
  try {
6542
- return parseManifest(readFileSync8(path, "utf-8"));
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(join11(tmpdir(), "hooks-install-"));
6562
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
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
- rmSync4(tmp, { recursive: true, force: true });
7254
+ rmSync3(tmp, { recursive: true, force: true });
6573
7255
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
6574
7256
  }
6575
- return join11(tmp, "repo");
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 existsSync11(value);
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
- rmSync4(repoDir, { recursive: true, force: true });
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 (!existsSync11(source)) {
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(readFileSync8(source, "utf-8"));
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
- mkdirSync7(dir, { recursive: true });
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 readFileSync9 } from "fs";
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(readFileSync9(path)).digest("hex");
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 HOOKS_API_KEY (vault-delivered, never stored)");
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 apiUrl = resolveApiUrl();
6741
- if (apiUrl) {
6742
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6743
- return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
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 apiUrl = resolveApiUrl();
6750
- const plan = await planSync();
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 (!apiUrl) {
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 { catalog, remoteLock } = await fetchRemoteState(apiUrl);
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(apiUrl, [...diff.added, ...diff.updated], remoteLock);
6771
- await commitSyncArtifacts(staged, apiUrl, remoteLock);
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
- init_config();
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 / 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 readFileSync10 } from "fs";
7567
- import { dirname as dirname6, join as join12 } from "path";
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 = join12(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
7572
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
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.port ?? DEFAULT_SERVE_PORT;
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, apiKey);
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
- init_config();
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 existsSync12, readFileSync as readFileSync11 } from "fs";
7843
- import { join as join13, dirname as dirname7 } from "path";
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 || !existsSync12(hookScript)) {
8542
+ if (!resolved || !hookScript || !existsSync14(hookScript)) {
7850
8543
  throw new Error(`Hook '${name}' not found`);
7851
8544
  }
7852
- const content = readFileSync11(hookScript);
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 = existsSync12(settingsPath);
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 (!existsSync12(custom.scriptPath)) {
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 (!existsSync12(join13(hookDir, "src", "hook.ts"))) {
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(readFileSync11(settingsPath, "utf-8"));
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 = join13(hookPath, "README.md");
8807
+ const readmePath = join16(hookPath, "README.md");
8115
8808
  let readme = "";
8116
- if (existsSync12(readmePath)) {
8117
- readme = readFileSync11(readmePath, "utf-8");
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: existsSync12(settingsPath),
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 (existsSync12(settingsPath))
8447
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
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: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
9148
+ const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
8456
9149
  const { dirname: dirname8 } = await import("path");
8457
- mkdirSync8(dirname8(settingsPath), { recursive: true });
8458
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
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 (existsSync12(settingsPath))
8470
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
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: writeFileSync7 } = await import("fs");
8477
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
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 = join13(__dirname3, rel);
8835
- if (existsSync12(p)) {
8836
- pkg = JSON.parse(readFileSync11(p, "utf-8"));
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 || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
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(dataDir = getEventsDataDir()) {
9036
- this.dataDir = dataDir;
9037
- this.runtime = localJsonRuntime(dataDir);
9038
- this.channelsPath = join(dataDir, "channels.json");
9039
- this.eventsPath = join(dataDir, "events.json");
9040
- this.deliveriesPath = join(dataDir, "deliveries.json");
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 (!existsSync(path)) {
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(dataDir = getEventsDataDir()) {
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 ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
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(dataDir) {
9265
- const store = new JsonEventsStore(dataDir);
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(dataDir, fileName, records) {
9308
- const path = join(dataDir, fileName);
9309
- return { path, exists: existsSync(path), records };
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((resolve, reject) => {
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
- resolve(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
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((resolve) => {
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
- resolve({
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
- resolve({
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 existsSync13, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
10459
- import { join as join14, dirname as dirname8 } from "path";
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 = existsSync13(join14(__dirname4, "..", "package.json")) ? join14(__dirname4, "..", "package.json") : join14(__dirname4, "..", "..", "package.json");
11643
- var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
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 readFileSync12(tokenFile, "utf-8").trim();
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 config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: apiKeyRef };
11753
- const path = writeConfig2(config);
12551
+ const apiUrl = options.apiUrl.replace(/\/+$/, "");
11754
12552
  if (options.json) {
11755
- console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref, config_path: path }));
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 configured
12566
+ \u2713 Remote registry configuration for ${apiUrl}
11760
12567
  `));
11761
- console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
11762
- console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref}`);
11763
- console.log(` ${chalk2.dim("Config:")} ${path}`);
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(" Presence of an API URL selects the remote registry; absence means local."));
11766
- console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
11767
- console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
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:")} ~/.hasna/hooks/profiles/${profile.agent_id}.json`);
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 || !existsSync13(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 = readFileSync12(hookScript);
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 { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
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
- rmSync5(customHookDir2(custom.name), { recursive: true, force: true });
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 apiUrl = resolveApiUrl2();
12049
- if (!apiUrl)
12050
- throw new Error(`Cannot install '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
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, apiUrl);
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 ? "custom (~/.hasna/hooks/hooks/) overrides the bundled registry" : "bundled registry";
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 = existsSync13(settingsPath);
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(readFileSync12(settingsPath, "utf-8"));
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 (!existsSync13(custom.scriptPath)) {
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 = join14(hookDir, "src", "hook.ts");
12411
- if (!existsSync13(hookScript)) {
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(readFileSync12(settingsPath, "utf-8"));
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 { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
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 apiUrl = resolveApiUrl2();
12508
- if (!apiUrl)
12509
- throw new Error(`Cannot update '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
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, apiUrl);
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 = join14(hookPath, "README.md");
13405
+ const readmePath = join17(hookPath, "README.md");
12586
13406
  let readme = "";
12587
- if (existsSync13(readmePath)) {
12588
- readme = readFileSync12(readmePath, "utf-8");
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: writeFileSync7 } = await import("fs");
12773
- writeFileSync7(options.output, json2 + `
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: readFileSync13 } = await import("fs");
13605
+ const { readFileSync: readFileSync12 } = await import("fs");
12786
13606
  try {
12787
- raw = readFileSync13(file, "utf-8");
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("Query hook event logs from SQLite (~/.hasna/hooks/hooks.db)");
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 HASNA_HOOKS_API_KEY / HOOKS_API_KEY only \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
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 API URL is configured)").action(async (options) => {
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();