@hasna/hooks 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,6 +181,17 @@ export HASNA_HOOKS_API_KEY_REF=<vault-key-name> # resolved through the vault a
181
181
  secrets exec <vault-key-name> --as HASNA_HOOKS_API_KEY -- hooks serve
182
182
  ```
183
183
 
184
+ The owner-only `~/.hasna/hooks/config/credentials` file also accepts a reference:
185
+
186
+ ```dotenv
187
+ HASNA_HOOKS_API_URL=https://registry.example.com
188
+ HASNA_HOOKS_API_KEY_REF=your-team/hooks/live/api-key
189
+ ```
190
+
191
+ Keep this file at mode `0400` or `0600`, and configure Secrets through its normal independent credential resolver. Hooks resolves the selected reference through the Secrets SDK for each registry request and each server publish-key check. References retain their existing precedence: a locked Keychain remains a refusal ahead of a file reference, and a missing reference never falls through to a stale literal key. A file cannot contain both a literal key and a reference.
192
+
193
+ `hooks list` displays catalog metadata and does not prove that the remote registry is reachable. Use `hooks sync --dry-run` to exercise registry reads without changing installed hooks. SDK consumers use `createHooksClient()`; callers working directly with transport metadata must await `resolveHooksRequestAuthority()` before sending a request. The server uses `resolveHooksServePublishKeyAsync()` for references.
194
+
184
195
  **Cloudflare provisioning.** `hooks cf deploy` creates the D1 database and R2 bucket via the Cloudflare API, then prints the exact wrangler commands for the worker upload (the worker needs the workerd target, which only wrangler can bundle):
185
196
 
186
197
  ```bash
@@ -193,8 +204,8 @@ The worker (`src/cf/worker.ts`) implements the same API routes against D1 + R2,
193
204
 
194
205
  ## Storage
195
206
 
196
- Hooks stores data locally by default in `~/.hasna/hooks/` and uses SQLite
197
- directly for hook event history. The package owns its database schema and
207
+ When local mode is explicitly selected, Hooks stores data in `~/.hasna/hooks/`
208
+ and uses SQLite for hook event history. The package owns its database schema and
198
209
  migrations; it does not depend on the deprecated shared runtime or its CLI.
199
210
  The repo includes its own PostgreSQL migration definitions for the optional
200
211
  `hooks storage push|pull|sync` commands. Use the `hooks log` commands to inspect
package/bin/hooks-mcp.js CHANGED
@@ -1418,14 +1418,14 @@ var init_redact = __esm(() => {
1418
1418
  ];
1419
1419
  });
1420
1420
 
1421
- // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
1421
+ // ../contracts/dist/client/transport.js
1422
1422
  import { isIP } from "net";
1423
1423
  import { spawnSync } from "child_process";
1424
- import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
1425
- import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
1426
- import { createRequire } from "module";
1424
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
1425
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
1427
1426
  import { hostname as osHostname } from "os";
1428
- import { isAbsolute as isAbsolute2, join as join9 } from "path";
1427
+ import { isAbsolute as isAbsolute2, join as join22 } from "path";
1428
+ import { createRequire } from "module";
1429
1429
  function envToken(name) {
1430
1430
  return name.toUpperCase().replace(/-/g, "_");
1431
1431
  }
@@ -1455,14 +1455,14 @@ function hasnaHomeDir(env) {
1455
1455
  if (override)
1456
1456
  return override;
1457
1457
  const home = homeDir(env);
1458
- return home ? join9(home, HASNA_HOME_DIR) : null;
1458
+ return home ? join22(home, HASNA_HOME_DIR) : null;
1459
1459
  }
1460
1460
  function appConfigDir(name, env) {
1461
1461
  const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
1462
1462
  if (configRoot)
1463
- return join9(configRoot, name);
1463
+ return join22(configRoot, name);
1464
1464
  const root = hasnaHomeDir(env);
1465
- return root ? join9(root, name, CONFIG_SUBDIR) : null;
1465
+ return root ? join22(root, name, CONFIG_SUBDIR) : null;
1466
1466
  }
1467
1467
  function credentialDiskSourceList(name, env, profile = null) {
1468
1468
  if (!SAFE_APP_SLUG.test(name))
@@ -1471,7 +1471,7 @@ function credentialDiskSourceList(name, env, profile = null) {
1471
1471
  if (!directory)
1472
1472
  return [];
1473
1473
  const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
1474
- return [{ path: join9(directory, file), tier: "disk" }];
1474
+ return [{ path: join22(directory, file), tier: "disk" }];
1475
1475
  }
1476
1476
  function credentialDiskSources(name, env) {
1477
1477
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -1525,7 +1525,7 @@ function readAppConfigFile(path) {
1525
1525
  };
1526
1526
  let fd = -1;
1527
1527
  try {
1528
- fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
1528
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
1529
1529
  } catch (error) {
1530
1530
  const code = error.code;
1531
1531
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -1535,7 +1535,7 @@ function readAppConfigFile(path) {
1535
1535
  unsafe(`the path could not be opened (${code ?? "unknown error"})`);
1536
1536
  }
1537
1537
  try {
1538
- const before = fstatSync(fd);
1538
+ const before = fstatSync2(fd);
1539
1539
  if (!before.isFile())
1540
1540
  unsafe("the path is not a regular file");
1541
1541
  if (!configFileModeAllowed(before.mode)) {
@@ -1546,8 +1546,8 @@ function readAppConfigFile(path) {
1546
1546
  unsafe("the file is not owned by the current user");
1547
1547
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
1548
1548
  unsafe("the file exceeds the size limit");
1549
- const bytes = readFileSync7(fd);
1550
- const after = fstatSync(fd);
1549
+ const bytes = readFileSync22(fd);
1550
+ const after = fstatSync2(fd);
1551
1551
  if (!configFileReadsCoherent(before, after)) {
1552
1552
  unsafe("the file changed while being read");
1553
1553
  }
@@ -1557,20 +1557,28 @@ function readAppConfigFile(path) {
1557
1557
  closeSync2(fd);
1558
1558
  }
1559
1559
  }
1560
- function readCredentialFile(path, apiKeyKeys) {
1560
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
1561
1561
  const parsed = readAppConfigFile(path);
1562
1562
  if (!parsed)
1563
1563
  return null;
1564
- for (const key of apiKeyKeys) {
1564
+ for (const key of [...apiKeyKeys, pointerKey]) {
1565
1565
  if (parsed.unusable.has(key)) {
1566
1566
  throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
1567
1567
  }
1568
1568
  }
1569
1569
  const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
1570
+ const pointer = parsed.values.get(pointerKey)?.trim();
1571
+ if (pointer !== undefined) {
1572
+ if (!VAULT_POINTER_SHAPE.test(pointer))
1573
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
1574
+ if (values.length)
1575
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
1576
+ return { apiKey: "", pointerVaultKey: pointer };
1577
+ }
1570
1578
  if (new Set(values).size > 1) {
1571
1579
  throw new CredentialFileUnsafeError(path, "credential aliases disagree");
1572
1580
  }
1573
- return values[0] ?? null;
1581
+ return values[0] === undefined ? null : { apiKey: values[0] };
1574
1582
  }
1575
1583
  function appConfigDiskValue(name, env, keys) {
1576
1584
  const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
@@ -1827,12 +1835,13 @@ function resolveCredential(name, env, options = {}) {
1827
1835
  }
1828
1836
  const paths = profileDiskSources(name, env, profile);
1829
1837
  for (const path of paths) {
1830
- const value = readCredentialFile(path, apiKeyKeys);
1838
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
1831
1839
  if (value) {
1832
- assertUsableCredential(name, path, value);
1840
+ if (!value.pointerVaultKey)
1841
+ assertUsableCredential(name, path, value.apiKey);
1833
1842
  return sealCredential({
1834
- apiKey: value,
1835
- tier: "profile",
1843
+ ...value,
1844
+ tier: value.pointerVaultKey ? "pointer" : "profile",
1836
1845
  source: path,
1837
1846
  deliberate: true,
1838
1847
  diskCandidates: paths,
@@ -1840,7 +1849,7 @@ function resolveCredential(name, env, options = {}) {
1840
1849
  });
1841
1850
  }
1842
1851
  }
1843
- 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);
1852
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
1844
1853
  }
1845
1854
  const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
1846
1855
  const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
@@ -1865,17 +1874,27 @@ function resolveCredential(name, env, options = {}) {
1865
1874
  });
1866
1875
  }
1867
1876
  const diskSourceList = credentialDiskSourceList(name, env, null);
1868
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
1877
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
1869
1878
  if (diskHits.length > 0) {
1870
1879
  const winner = diskHits[0];
1871
- assertUsableCredential(name, winner.src.path, winner.value);
1880
+ if (winner.value.pointerVaultKey) {
1881
+ return sealCredential({
1882
+ ...winner.value,
1883
+ tier: "pointer",
1884
+ source: winner.src.path,
1885
+ deliberate: false,
1886
+ diskCandidates: diskPaths,
1887
+ warning: null
1888
+ });
1889
+ }
1890
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
1872
1891
  const divergentSources = [
1873
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
1874
- ...envHit && envHit.value !== winner.value ? [envHit.key] : []
1892
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
1893
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
1875
1894
  ];
1876
1895
  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;
1877
1896
  return sealCredential({
1878
- apiKey: winner.value,
1897
+ apiKey: winner.value.apiKey,
1879
1898
  tier: winner.src.tier,
1880
1899
  source: winner.src.path,
1881
1900
  deliberate: false,
@@ -2098,8 +2117,9 @@ function credentialDiskSourcesForMessage(name, env) {
2098
2117
  const paths = credentialDiskSources(name, env);
2099
2118
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2100
2119
  }
2101
- 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;
2120
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_PACKAGE_BYTES, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
2102
2121
  var init_transport = __esm(() => {
2122
+ MAX_PACKAGE_BYTES = 1024 * 1024;
2103
2123
  CredentialResolutionError = class CredentialResolutionError extends Error {
2104
2124
  appName;
2105
2125
  attempted;
@@ -2127,7 +2147,6 @@ var init_transport = __esm(() => {
2127
2147
  INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
2128
2148
  CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
2129
2149
  AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2130
- SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
2131
2150
  requireSecretsSdk = createRequire(import.meta.url);
2132
2151
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
2133
2152
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -2385,7 +2404,7 @@ __export(exports_run, {
2385
2404
  });
2386
2405
  import { randomBytes as randomBytes2 } from "crypto";
2387
2406
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2388
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join10 } from "path";
2407
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join9 } from "path";
2389
2408
  function extensionOf(scriptPath) {
2390
2409
  const base = basename2(scriptPath);
2391
2410
  const dot = base.lastIndexOf(".");
@@ -2456,7 +2475,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
2456
2475
  async function executeVerifiedScript(options) {
2457
2476
  const scriptDir = dirname6(options.scriptPath);
2458
2477
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
2459
- const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2478
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2460
2479
  let proc = null;
2461
2480
  let timedOut = false;
2462
2481
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -2664,8 +2683,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2664
2683
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2665
2684
  import { z as z2 } from "zod";
2666
2685
  init_registry();
2667
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2668
- import { join as join11, dirname as dirname7 } from "path";
2686
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2687
+ import { join as join10, dirname as dirname7 } from "path";
2669
2688
  import { fileURLToPath as fileURLToPath3 } from "url";
2670
2689
 
2671
2690
  // src/lib/installer.ts
@@ -2708,7 +2727,7 @@ function mementosCommandSuffix(value) {
2708
2727
  return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
2709
2728
  }
2710
2729
  function retainedMementosRegistration(commands) {
2711
- const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
2730
+ const values = commands.map((command) => command.trim()).filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
2712
2731
  const args = command.trim().split(/\s+/).slice(3);
2713
2732
  const flags = {};
2714
2733
  const seen = new Set;
@@ -3436,8 +3455,36 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
3436
3455
  if (!settings.hooks)
3437
3456
  settings.hooks = {};
3438
3457
  const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
3439
- removeHookFromAllEvents(settings, name);
3440
3458
  const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
3459
+ if (target === "codex") {
3460
+ const positions = [];
3461
+ for (const [event, groups] of Object.entries(settings.hooks)) {
3462
+ for (const entry of groups) {
3463
+ for (let index = 0;index < entry.hooks.length; index++) {
3464
+ const command = entry.hooks[index]?.command;
3465
+ const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
3466
+ if (match && (match[1] ?? match[2]) === name) {
3467
+ if (registrationName(command) !== name)
3468
+ throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
3469
+ positions.push({ event, entry, index });
3470
+ }
3471
+ }
3472
+ }
3473
+ }
3474
+ if (positions.length) {
3475
+ if (positions.length !== uniqueEventKeys.length || uniqueEventKeys.some((event) => positions.filter((position) => position.event === event).length !== 1) || positions.some((position) => (position.entry.matcher ?? "") !== (meta.matcher ?? ""))) {
3476
+ throw new Error("Codex hook events, matchers or duplicate registrations require explicit reconciliation; no settings were changed.");
3477
+ }
3478
+ for (const { entry, index } of positions) {
3479
+ entry.hooks[index] = { ...entry.hooks[index], type: "command", command: hookCommand };
3480
+ if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0)
3481
+ entry.hooks[index].timeout = meta.timeoutSeconds;
3482
+ }
3483
+ writeSettings(settings, scope, target);
3484
+ return;
3485
+ }
3486
+ }
3487
+ removeHookFromAllEvents(settings, name);
3441
3488
  for (const eventKey of uniqueEventKeys) {
3442
3489
  if (!settings.hooks[eventKey])
3443
3490
  settings.hooks[eventKey] = [];
@@ -3642,6 +3689,7 @@ function rethrowHooksAuthorityFailure(error) {
3642
3689
  }
3643
3690
  throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
3644
3691
  }
3692
+ var authorityBindings = new WeakMap;
3645
3693
  function resolveHooksTransport(env = process.env, options = {}) {
3646
3694
  if (selectsHooksLocalStore(env)) {
3647
3695
  announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
@@ -3655,7 +3703,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
3655
3703
  try {
3656
3704
  credential = resolveCredential("hooks", resolverEnv, credentialOptions);
3657
3705
  resolution = resolveClientTransport("hooks", resolverEnv, {
3658
- credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
3706
+ credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
3659
3707
  });
3660
3708
  } catch (error) {
3661
3709
  rethrowHooksAuthorityFailure(error);
@@ -3663,18 +3711,20 @@ function resolveHooksTransport(env = process.env, options = {}) {
3663
3711
  if (!credential) {
3664
3712
  throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
3665
3713
  }
3714
+ const authority = {
3715
+ origin: hooksRegistryOrigin(resolution.baseUrl),
3716
+ apiKey: credential.apiKey,
3717
+ apiUrlSource: resolution.apiUrlSource,
3718
+ apiKeySource: credential.source,
3719
+ apiKeyTier: credential.tier,
3720
+ v1BaseUrl: resolution.baseUrl,
3721
+ warning: resolution.warning
3722
+ };
3723
+ authorityBindings.set(authority, { env, options, credential });
3666
3724
  return {
3667
3725
  mode: "remote",
3668
3726
  source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
3669
- authority: {
3670
- origin: hooksRegistryOrigin(resolution.baseUrl),
3671
- apiKey: credential.apiKey,
3672
- apiUrlSource: resolution.apiUrlSource,
3673
- apiKeySource: credential.source,
3674
- apiKeyTier: credential.tier,
3675
- v1BaseUrl: resolution.baseUrl,
3676
- warning: resolution.warning
3677
- }
3727
+ authority
3678
3728
  };
3679
3729
  }
3680
3730
 
@@ -4126,9 +4176,9 @@ var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
4126
4176
  var pkg = { name: "@hasna/hooks", version: "0.0.0" };
4127
4177
  try {
4128
4178
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
4129
- const p = join11(__dirname3, rel);
4179
+ const p = join10(__dirname3, rel);
4130
4180
  if (existsSync11(p)) {
4131
- pkg = JSON.parse(readFileSync8(p, "utf-8"));
4181
+ pkg = JSON.parse(readFileSync7(p, "utf-8"));
4132
4182
  break;
4133
4183
  }
4134
4184
  }
@@ -4140,7 +4190,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
4140
4190
  if (!resolved || !hookScript || !existsSync11(hookScript)) {
4141
4191
  throw new Error(`Hook '${name}' not found`);
4142
4192
  }
4143
- const content = readFileSync8(hookScript);
4193
+ const content = readFileSync7(hookScript);
4144
4194
  const sha = sha256Of(content);
4145
4195
  const check = checkScriptHash(name, sha);
4146
4196
  if (!check.ok) {
@@ -4366,14 +4416,14 @@ function createHooksServer() {
4366
4416
  continue;
4367
4417
  }
4368
4418
  const hookDir = getHookPath(name);
4369
- if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
4419
+ if (!existsSync11(join10(hookDir, "src", "hook.ts"))) {
4370
4420
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4371
4421
  hookHealthy = false;
4372
4422
  }
4373
4423
  }
4374
4424
  if (meta && settingsExist) {
4375
4425
  try {
4376
- const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4426
+ const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4377
4427
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
4378
4428
  const eventName = meta.event.split(":")[0];
4379
4429
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -4410,10 +4460,10 @@ function createHooksServer() {
4410
4460
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4411
4461
  }
4412
4462
  const hookPath = getHookPath(name);
4413
- const readmePath = join11(hookPath, "README.md");
4463
+ const readmePath = join10(hookPath, "README.md");
4414
4464
  let readme = "";
4415
4465
  if (existsSync11(readmePath)) {
4416
- readme = readFileSync8(readmePath, "utf-8");
4466
+ readme = readFileSync7(readmePath, "utf-8");
4417
4467
  }
4418
4468
  if (verbose) {
4419
4469
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -4743,7 +4793,7 @@ function createHooksServer() {
4743
4793
  let settings = {};
4744
4794
  try {
4745
4795
  if (existsSync11(settingsPath))
4746
- settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4796
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4747
4797
  } catch {}
4748
4798
  if (!settings.hooks)
4749
4799
  settings.hooks = {};
@@ -4766,7 +4816,7 @@ function createHooksServer() {
4766
4816
  let settings = {};
4767
4817
  try {
4768
4818
  if (existsSync11(settingsPath))
4769
- settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4819
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4770
4820
  } catch {}
4771
4821
  if (settings.hooks?.__disabled) {
4772
4822
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);