@hasna/hooks 0.9.0 → 0.9.1

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/bin/index.js CHANGED
@@ -2087,10 +2087,10 @@ var init_registry = __esm(() => {
2087
2087
  name: "trash-guard",
2088
2088
  displayName: "Trash Guard",
2089
2089
  description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
2090
- version: "0.1.0",
2090
+ version: "0.1.1",
2091
2091
  category: "Git Safety",
2092
2092
  event: "PreToolUse",
2093
- matcher: "Bash",
2093
+ matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
2094
2094
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
2095
2095
  rewritesInput: true,
2096
2096
  timeoutSeconds: 5
@@ -4919,14 +4919,30 @@ function runRetention(db, days) {
4919
4919
  // src/db/index.ts
4920
4920
  var exports_db = {};
4921
4921
  __export(exports_db, {
4922
+ refuseLocalStore: () => refuseLocalStore,
4923
+ localStoreRefusalMessage: () => localStoreRefusalMessage,
4924
+ isLocalStoreRefused: () => isLocalStoreRefused,
4922
4925
  getDbPath: () => getDbPath,
4923
4926
  getDb: () => getDb,
4924
4927
  createTestDb: () => createTestDb,
4925
- closeDb: () => closeDb
4928
+ closeDb: () => closeDb,
4929
+ allowLocalStore: () => allowLocalStore
4926
4930
  });
4927
4931
  import { Database } from "bun:sqlite";
4928
4932
  import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
4929
4933
  import { join as join9 } from "path";
4934
+ function refuseLocalStore(message) {
4935
+ localStoreRefusal = message;
4936
+ }
4937
+ function allowLocalStore() {
4938
+ localStoreRefusal = null;
4939
+ }
4940
+ function isLocalStoreRefused() {
4941
+ return localStoreRefusal !== null;
4942
+ }
4943
+ function localStoreRefusalMessage() {
4944
+ return localStoreRefusal;
4945
+ }
4930
4946
  function resolveDataDir() {
4931
4947
  const effective = getEffectiveDataRoot();
4932
4948
  const oldDir = join9(getHomeDir(), ".hooks");
@@ -4950,6 +4966,8 @@ function ensureDir(dbPath) {
4950
4966
  }
4951
4967
  }
4952
4968
  function getDb() {
4969
+ if (localStoreRefusal !== null)
4970
+ throw new Error(localStoreRefusal);
4953
4971
  if (instance)
4954
4972
  return instance;
4955
4973
  const dbPath = getDbPath();
@@ -4987,7 +5005,7 @@ function createTestDb() {
4987
5005
  db.exec("PRAGMA foreign_keys=ON");
4988
5006
  return db;
4989
5007
  }
4990
- var instance = null;
5008
+ var instance = null, localStoreRefusal = null;
4991
5009
  var init_db = __esm(() => {
4992
5010
  init_app_home();
4993
5011
  init_migrations();
@@ -5026,6 +5044,9 @@ async function sha256File(path) {
5026
5044
  const buf = await readFile2(path);
5027
5045
  return sha256Of(buf);
5028
5046
  }
5047
+ function storeDb() {
5048
+ return isLocalStoreRefused() ? null : getDb();
5049
+ }
5029
5050
  function getHookRecord(db, name) {
5030
5051
  return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
5031
5052
  }
@@ -5126,26 +5147,28 @@ function removePinnedHook(name) {
5126
5147
  }
5127
5148
  function pinInstalledHook(name, version, sha256, source, sourceRef) {
5128
5149
  const now2 = new Date().toISOString();
5129
- const db = getDb();
5130
- upsertHookRecord(db, {
5131
- name,
5132
- version,
5133
- sha256,
5134
- source_type: source,
5135
- source_ref: sourceRef ?? null,
5136
- last_verified_at: now2
5137
- });
5150
+ const db = storeDb();
5151
+ if (db) {
5152
+ upsertHookRecord(db, {
5153
+ name,
5154
+ version,
5155
+ sha256,
5156
+ source_type: source,
5157
+ source_ref: sourceRef ?? null,
5158
+ last_verified_at: now2
5159
+ });
5160
+ }
5138
5161
  setPinnedHook(name, { version, sha256, source });
5139
5162
  }
5140
5163
  function removeHookFromStore(name) {
5141
5164
  const removedPin = removePinnedHook(name);
5142
- const db = getDb();
5143
- const removedRecord = removeHookRecord(db, name);
5165
+ const db = storeDb();
5166
+ const removedRecord = db ? removeHookRecord(db, name) : false;
5144
5167
  return { removedPin, removedRecord };
5145
5168
  }
5146
5169
  function checkScriptHash(name, actual) {
5147
- const db = getDb();
5148
- const record = getHookRecord(db, name);
5170
+ const db = storeDb();
5171
+ const record = db ? getHookRecord(db, name) : null;
5149
5172
  const pin = getPinnedHook(name);
5150
5173
  const expected = record?.sha256 ?? pin?.sha256 ?? null;
5151
5174
  if (expected !== null && expected !== actual) {
@@ -5155,24 +5178,28 @@ function checkScriptHash(name, actual) {
5155
5178
  const now3 = new Date().toISOString();
5156
5179
  const version = pin?.version ?? record?.version ?? "0.0.0";
5157
5180
  const source = pin?.source ?? record?.source_type ?? "local";
5158
- upsertHookRecord(db, {
5159
- name,
5160
- version,
5161
- sha256: actual,
5162
- source_type: source,
5163
- last_verified_at: now3
5164
- });
5181
+ if (db) {
5182
+ upsertHookRecord(db, {
5183
+ name,
5184
+ version,
5185
+ sha256: actual,
5186
+ source_type: source,
5187
+ last_verified_at: now3
5188
+ });
5189
+ }
5165
5190
  setPinnedHook(name, { version, sha256: actual, source });
5166
5191
  return { ok: true, pinned: false, expected: null, actual, name };
5167
5192
  }
5168
5193
  const now2 = new Date().toISOString();
5169
- upsertHookRecord(db, {
5170
- name,
5171
- version: record?.version ?? pin?.version ?? "0.0.0",
5172
- sha256: expected,
5173
- source_type: record?.source_type ?? pin?.source ?? "local",
5174
- last_verified_at: now2
5175
- });
5194
+ if (db) {
5195
+ upsertHookRecord(db, {
5196
+ name,
5197
+ version: record?.version ?? pin?.version ?? "0.0.0",
5198
+ sha256: expected,
5199
+ source_type: record?.source_type ?? pin?.source ?? "local",
5200
+ last_verified_at: now2
5201
+ });
5202
+ }
5176
5203
  return { ok: true, pinned: true, expected, actual, name };
5177
5204
  }
5178
5205
  async function verifyScriptHash(name, scriptPath) {
@@ -5180,14 +5207,16 @@ async function verifyScriptHash(name, scriptPath) {
5180
5207
  }
5181
5208
  function retrustHook(name, scriptPath, version, source) {
5182
5209
  const actual = sha256Of(readFileSync3(scriptPath));
5183
- const db = getDb();
5184
- upsertHookRecord(db, {
5185
- name,
5186
- version,
5187
- sha256: actual,
5188
- source_type: source,
5189
- last_verified_at: new Date().toISOString()
5190
- });
5210
+ const db = storeDb();
5211
+ if (db) {
5212
+ upsertHookRecord(db, {
5213
+ name,
5214
+ version,
5215
+ sha256: actual,
5216
+ source_type: source,
5217
+ last_verified_at: new Date().toISOString()
5218
+ });
5219
+ }
5191
5220
  setPinnedHook(name, { version, sha256: actual, source });
5192
5221
  return { ok: true, pinned: true, expected: actual, actual, name };
5193
5222
  }
@@ -6091,7 +6120,7 @@ var init_profiles = __esm(() => {
6091
6120
  init_app_home();
6092
6121
  });
6093
6122
 
6094
- // ../contracts/dist/client/transport.js
6123
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
6095
6124
  import { isIP as isIP2 } from "net";
6096
6125
  import { spawnSync } from "child_process";
6097
6126
  import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
@@ -6771,7 +6800,7 @@ function credentialDiskSourcesForMessage(name, env) {
6771
6800
  const paths = credentialDiskSources(name, env);
6772
6801
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
6773
6802
  }
6774
- 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_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
6803
+ 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_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
6775
6804
  var init_transport = __esm(() => {
6776
6805
  CredentialResolutionError = class CredentialResolutionError extends Error {
6777
6806
  appName;
@@ -6800,7 +6829,7 @@ var init_transport = __esm(() => {
6800
6829
  INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
6801
6830
  CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
6802
6831
  AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
6803
- SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
6832
+ SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
6804
6833
  requireSecretsSdk2 = createRequire2(import.meta.url);
6805
6834
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
6806
6835
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -6844,6 +6873,15 @@ function hasHooksEnvAuthorityIntent(env = process.env) {
6844
6873
  function selectsHooksLocalStore(env = process.env) {
6845
6874
  return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
6846
6875
  }
6876
+ function hooksFailClosedLine(detail) {
6877
+ return `REMOTE_API_CONFIG_MISSING: hooks: no registry credential resolved from ${HOOKS_CREDENTIAL_TIERS} ` + `(with HASNA_HOOKS_API_URL or the fleet gateway default), and local mode is not enabled ` + `(${HOOKS_LOCAL_OPT_IN_HINT}) \u2014 refusing to silently fall back to local storage.` + (detail ? ` ${detail}` : "");
6878
+ }
6879
+ function hooksHostedRouteLocalStoreRefusal(authority) {
6880
+ return "REMOTE_COMMAND_UNSUPPORTED: this command is local-only \u2014 it reads the on-box SQLite store " + `(hooks.db), which the hosted hooks registry authority (${authority}) does not serve; local SQLite ` + `is opt-in only (${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default \u2014 refusing to open a local ` + "store on the hosted route.";
6881
+ }
6882
+ function hooksEventSinkRefusal() {
6883
+ return "REMOTE_COMMAND_UNSUPPORTED: hook event not recorded \u2014 the hook event log is local-only (on-box " + "SQLite hooks.db) and the hosted hooks registry has no event route; local SQLite is opt-in only " + `(${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default. Nothing was written.`;
6884
+ }
6847
6885
  function hooksResolverEnv(env) {
6848
6886
  const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
6849
6887
  if (blanks.length === 0)
@@ -6868,7 +6906,7 @@ function hooksResolverInputs(env, credentials = {}) {
6868
6906
  }
6869
6907
  return { env: normalised, credentials: { ...credentials, keychain } };
6870
6908
  }
6871
- var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
6909
+ var HOOKS_LOCAL_OPT_IN_ENV_KEYS, HOOKS_CREDENTIAL_TIERS = "the Keychain item hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY", HOOKS_LOCAL_OPT_IN_HINT = "HASNA_HOOKS_LOCAL=1 (alias HOOKS_LOCAL=1)", CONTRACTS_AMBIENT_ENVIRONMENT;
6872
6910
  var init_local_opt_in = __esm(() => {
6873
6911
  init_transport();
6874
6912
  HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
@@ -6971,14 +7009,35 @@ var init_transport2 = __esm(() => {
6971
7009
  var exports_db_writer = {};
6972
7010
  __export(exports_db_writer, {
6973
7011
  writeHookEvent: () => writeHookEvent,
7012
+ resolveHookEventSink: () => resolveHookEventSink,
6974
7013
  resolveEventType: () => resolveEventType,
6975
7014
  recordHookRun: () => recordHookRun,
6976
- normalizeEventType: () => normalizeEventType
7015
+ normalizeEventType: () => normalizeEventType,
7016
+ __resetHookEventSinkNotice: () => __resetHookEventSinkNotice
6977
7017
  });
7018
+ function resolveHookEventSink(env = process.env) {
7019
+ if (isLocalStoreRefused())
7020
+ return { kind: "refused", reason: localStoreRefusalMessage() ?? hooksEventSinkRefusal() };
7021
+ if (selectsHooksLocalStore(env))
7022
+ return { kind: "local" };
7023
+ return { kind: "refused", reason: hooksEventSinkRefusal() };
7024
+ }
7025
+ function __resetHookEventSinkNotice() {
7026
+ sinkRefusalPrinted = false;
7027
+ }
6978
7028
  function nanoid2() {
6979
7029
  return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
6980
7030
  }
6981
7031
  function writeHookEvent(event) {
7032
+ const sink = resolveHookEventSink();
7033
+ if (sink.kind !== "local") {
7034
+ if (!sinkRefusalPrinted) {
7035
+ sinkRefusalPrinted = true;
7036
+ process.stderr.write(`[hooks db-writer] ${sink.reason}
7037
+ `);
7038
+ }
7039
+ return;
7040
+ }
6982
7041
  try {
6983
7042
  const db = getDb();
6984
7043
  const id = nanoid2();
@@ -7047,9 +7106,10 @@ function recordHookRun(record) {
7047
7106
  metadata: JSON.stringify(metadata)
7048
7107
  });
7049
7108
  }
7050
- var ALLOWED_EVENT_TYPES;
7109
+ var sinkRefusalPrinted = false, ALLOWED_EVENT_TYPES;
7051
7110
  var init_db_writer = __esm(() => {
7052
7111
  init_db();
7112
+ init_local_opt_in();
7053
7113
  init_redact();
7054
7114
  ALLOWED_EVENT_TYPES = new Set([
7055
7115
  "PreToolUse",
@@ -7065,7 +7125,8 @@ var init_db_writer = __esm(() => {
7065
7125
 
7066
7126
  // src/lib/hook-env.ts
7067
7127
  import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
7068
- import { existsSync as existsSync12, statSync } from "fs";
7128
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync } from "fs";
7129
+ import { tmpdir } from "os";
7069
7130
  function isDeniedEnvName(name) {
7070
7131
  const upper = name.toUpperCase();
7071
7132
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -7091,6 +7152,19 @@ function isUnsafePathEntry(entry, home) {
7091
7152
  return true;
7092
7153
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
7093
7154
  return true;
7155
+ const canonical = (path) => {
7156
+ try {
7157
+ return realpathSync2(path);
7158
+ } catch {
7159
+ return path;
7160
+ }
7161
+ };
7162
+ const resolved = canonical(entry);
7163
+ for (const root of [home, tmpdir(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
7164
+ const blocked = canonical(root);
7165
+ if (resolved === blocked || resolved.startsWith(blocked + "/"))
7166
+ return true;
7167
+ }
7094
7168
  try {
7095
7169
  if ((statSync(entry).mode & 2) !== 0)
7096
7170
  return true;
@@ -7210,6 +7284,7 @@ var init_hook_env = __esm(() => {
7210
7284
  DENY_CONTAINS = ["DATABASE_URL"];
7211
7285
  PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
7212
7286
  CONFIG_PROJECTIONS = [
7287
+ ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"],
7213
7288
  ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
7214
7289
  ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
7215
7290
  ["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
@@ -7389,7 +7464,7 @@ __export(exports_custom_install, {
7389
7464
  });
7390
7465
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
7391
7466
  import { join as join14 } from "path";
7392
- import { tmpdir } from "os";
7467
+ import { tmpdir as tmpdir2 } from "os";
7393
7468
  function manifestAt(dir) {
7394
7469
  const path = join14(dir, "manifest.json");
7395
7470
  if (!existsSync13(path))
@@ -7414,7 +7489,7 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7414
7489
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7415
7490
  }
7416
7491
  async function cloneToTemp(gitUrl) {
7417
- const tmp = mkdtempSync(join14(tmpdir(), "hooks-install-"));
7492
+ const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
7418
7493
  const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
7419
7494
  stdout: "pipe",
7420
7495
  stderr: "pipe"
@@ -7501,6 +7576,7 @@ __export(exports_sync, {
7501
7576
  syncHooks: () => syncHooks,
7502
7577
  stageSyncArtifacts: () => stageSyncArtifacts,
7503
7578
  planSync: () => planSync,
7579
+ installPinnedFromBundled: () => installPinnedFromBundled,
7504
7580
  fetchPinnedHook: () => fetchPinnedHook,
7505
7581
  commitSyncArtifacts: () => commitSyncArtifacts
7506
7582
  });
@@ -7664,7 +7740,7 @@ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
7664
7740
  return staged;
7665
7741
  }
7666
7742
  async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7667
- const db = getDb();
7743
+ const db = isLocalStoreRefused() ? null : getDb();
7668
7744
  for (const item of staged) {
7669
7745
  const entry = remoteLock.hooks[item.name];
7670
7746
  if (!entry)
@@ -7684,6 +7760,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7684
7760
  lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
7685
7761
  }
7686
7762
  writeLock(lock);
7763
+ if (!db)
7764
+ return;
7687
7765
  db.exec("BEGIN");
7688
7766
  try {
7689
7767
  for (const item of staged) {
@@ -7746,23 +7824,56 @@ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
7746
7824
  if (actual !== expectedSha) {
7747
7825
  throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
7748
7826
  }
7749
- const db = getDb();
7750
7827
  setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
7751
- upsertHookRecord(db, {
7828
+ if (!isLocalStoreRefused()) {
7829
+ upsertHookRecord(getDb(), {
7830
+ name,
7831
+ version,
7832
+ sha256: expectedSha,
7833
+ source_type: entry.source ?? "remote",
7834
+ source_ref: apiUrl,
7835
+ last_verified_at: new Date().toISOString()
7836
+ });
7837
+ }
7838
+ return {
7752
7839
  name,
7753
7840
  version,
7754
7841
  sha256: expectedSha,
7755
- source_type: entry.source ?? "remote",
7842
+ source: entry.source ?? "remote",
7756
7843
  source_ref: apiUrl,
7844
+ artifact,
7845
+ scriptPath
7846
+ };
7847
+ }
7848
+ async function installPinnedFromBundled(name, version) {
7849
+ const bundled = collectBundledCatalog().find((entry) => entry.name === name);
7850
+ if (!bundled) {
7851
+ throw new Error(`Hook '${name}' is not in the bundled registry`);
7852
+ }
7853
+ if (bundled.version !== version) {
7854
+ throw new Error(`Hook '${name}' version ${version} is not in the bundled registry (available: ${bundled.version})`);
7855
+ }
7856
+ const scriptPath = resolveScriptPath(name);
7857
+ if (!scriptPath) {
7858
+ throw new Error(`Hook '${name}' has no script in the bundled registry`);
7859
+ }
7860
+ const db = getDb();
7861
+ setPinnedHook(name, { version, sha256: bundled.sha256, source: "bundled", pinned: true });
7862
+ upsertHookRecord(db, {
7863
+ name,
7864
+ version,
7865
+ sha256: bundled.sha256,
7866
+ source_type: "bundled",
7867
+ source_ref: "bundled registry",
7757
7868
  last_verified_at: new Date().toISOString()
7758
7869
  });
7759
7870
  return {
7760
7871
  name,
7761
7872
  version,
7762
- sha256: expectedSha,
7763
- source: entry.source ?? "remote",
7764
- source_ref: apiUrl,
7765
- artifact,
7873
+ sha256: bundled.sha256,
7874
+ source: "bundled",
7875
+ source_ref: "bundled registry",
7876
+ artifact: null,
7766
7877
  scriptPath
7767
7878
  };
7768
7879
  }
@@ -8595,7 +8706,7 @@ function startServeServer(options) {
8595
8706
  return handleServeRequest(req, resolveHooksServePublishKey());
8596
8707
  }
8597
8708
  });
8598
- console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
8709
+ console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
8599
8710
  return server;
8600
8711
  }
8601
8712
  var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
@@ -8691,6 +8802,32 @@ async function provisionCloudflareResources(options) {
8691
8802
  }
8692
8803
  var CF_API = "https://api.cloudflare.com/client/v4";
8693
8804
 
8805
+ // src/mcp/authority.ts
8806
+ var exports_authority = {};
8807
+ __export(exports_authority, {
8808
+ resolveHooksMcpAuthority: () => resolveHooksMcpAuthority,
8809
+ decideHooksMcpAuthority: () => decideHooksMcpAuthority
8810
+ });
8811
+ function resolveHooksMcpAuthority(env = process.env) {
8812
+ const transport = resolveHooksTransport(env);
8813
+ if (transport.mode === "local" || !transport.authority) {
8814
+ return { route: "local", source: "local-opt-in" };
8815
+ }
8816
+ return { route: "hosted", v1BaseUrl: transport.authority.v1BaseUrl, source: transport.source };
8817
+ }
8818
+ function decideHooksMcpAuthority(env = process.env) {
8819
+ const authority = resolveHooksMcpAuthority(env);
8820
+ if (authority.route === "hosted") {
8821
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal(authority.v1BaseUrl));
8822
+ }
8823
+ return authority;
8824
+ }
8825
+ var init_authority = __esm(() => {
8826
+ init_db();
8827
+ init_local_opt_in();
8828
+ init_transport2();
8829
+ });
8830
+
8694
8831
  // src/mcp/server.ts
8695
8832
  var exports_server = {};
8696
8833
  __export(exports_server, {
@@ -9632,6 +9769,7 @@ async function startSSEServer(options = {}) {
9632
9769
  if (!loopback && !authToken) {
9633
9770
  throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
9634
9771
  }
9772
+ decideHooksMcpAuthority();
9635
9773
  const server = createHooksServer();
9636
9774
  const transports = new Map;
9637
9775
  function authorized2(headers) {
@@ -9681,6 +9819,13 @@ async function startSSEServer(options = {}) {
9681
9819
  });
9682
9820
  }
9683
9821
  async function startStdioServer() {
9822
+ try {
9823
+ decideHooksMcpAuthority();
9824
+ } catch (err) {
9825
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
9826
+ `);
9827
+ process.exit(1);
9828
+ }
9684
9829
  try {
9685
9830
  const server = createHooksServer();
9686
9831
  const transport = new StdioServerTransport;
@@ -9701,6 +9846,7 @@ var init_server = __esm(() => {
9701
9846
  init_registration();
9702
9847
  init_store();
9703
9848
  init_redact();
9849
+ init_authority();
9704
9850
  init_storage();
9705
9851
  __dirname3 = dirname7(fileURLToPath4(import.meta.url));
9706
9852
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
@@ -10892,11 +11038,11 @@ function setPath(input, path, replacement) {
10892
11038
  if (last && last in cursor)
10893
11039
  cursor[last] = replacement;
10894
11040
  }
11041
+ var MAX_PACKAGE_BYTES = 1024 * 1024;
10895
11042
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
10896
11043
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
10897
11044
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
10898
11045
  var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
10899
- var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
10900
11046
  var requireSecretsSdk = createRequire(import.meta.url);
10901
11047
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
10902
11048
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
@@ -12637,6 +12783,7 @@ init_app_home();
12637
12783
  init_config();
12638
12784
  init_local_opt_in();
12639
12785
  init_transport2();
12786
+ init_db();
12640
12787
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
12641
12788
  var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
12642
12789
  var pkgPath = existsSync15(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
@@ -12671,6 +12818,19 @@ function truncateText2(value, max = 96) {
12671
12818
  function projectLogRows2(rows) {
12672
12819
  return rows.map((row) => projectEventRowForRead({ ...row }));
12673
12820
  }
12821
+ async function openLocalStoreOrExit(json2) {
12822
+ try {
12823
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
12824
+ return getDb2();
12825
+ } catch (error) {
12826
+ const message = error instanceof Error ? error.message : String(error);
12827
+ if (json2)
12828
+ console.log(JSON.stringify({ error: message }));
12829
+ else
12830
+ console.error(chalk2.red(`\u2717 ${message}`));
12831
+ process.exit(1);
12832
+ }
12833
+ }
12674
12834
  function readToken(tokenFile) {
12675
12835
  if (tokenFile) {
12676
12836
  try {
@@ -12735,6 +12895,11 @@ function suggestHooks(name, max = 3) {
12735
12895
  }
12736
12896
  program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
12737
12897
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
12898
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
12899
+ console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
12900
+ ` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
12901
+ process.exit(1);
12902
+ }
12738
12903
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
12739
12904
  });
12740
12905
  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) => {
@@ -12970,6 +13135,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
12970
13135
  const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
12971
13136
  const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
12972
13137
  const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13138
+ const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
12973
13139
  const { readFileSync: readFileSyncFs } = await import("fs");
12974
13140
  function parseNameVersion(arg) {
12975
13141
  if (isCustomSource2(arg))
@@ -13062,17 +13228,13 @@ Dry run \u2014 would install (${scope}, ${target}):
13062
13228
  for (const { arg, pinned } of pinnedRequests) {
13063
13229
  try {
13064
13230
  const transport = resolveHooksTransport2();
13065
- if (transport.mode !== "remote" || !transport.authority) {
13066
- 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)`);
13067
- }
13068
- const { origin, apiKey } = transport.authority;
13069
- const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13070
- const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
13231
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
13071
13232
  installedPinned.push(pinnedInstall.name);
13072
13233
  if (options.json) {
13073
13234
  console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
13074
13235
  } else {
13075
- console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from registry (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
13236
+ const registryLabel = transport.mode === "remote" ? "registry" : "bundled registry";
13237
+ console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from ${registryLabel} (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
13076
13238
  }
13077
13239
  } catch (error) {
13078
13240
  const message = error instanceof Error ? error.message : String(error);
@@ -13547,8 +13709,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13547
13709
  }
13548
13710
  const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
13549
13711
  const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13550
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13712
+ const { getDb: getDb2, isLocalStoreRefused: isLocalStoreRefused2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13551
13713
  const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
13714
+ const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13552
13715
  const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
13553
13716
  const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
13554
13717
  function parseNameVersion(arg) {
@@ -13571,12 +13734,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13571
13734
  if (pinned) {
13572
13735
  try {
13573
13736
  const transport = resolveHooksTransport2();
13574
- if (transport.mode !== "remote" || !transport.authority) {
13575
- 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)`);
13576
- }
13577
- const { origin, apiKey } = transport.authority;
13578
- const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13579
- const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
13737
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
13580
13738
  const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
13581
13739
  if (!registered.success) {
13582
13740
  results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
@@ -13601,13 +13759,15 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13601
13759
  if (result.success && resolved) {
13602
13760
  const hash = await sha256File2(resolved.scriptPath);
13603
13761
  setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
13604
- upsertHookRecord2(getDb2(), {
13605
- name: resolved.name,
13606
- version: resolved.version,
13607
- sha256: hash,
13608
- source_type: resolved.source,
13609
- last_verified_at: new Date().toISOString()
13610
- });
13762
+ if (!isLocalStoreRefused2()) {
13763
+ upsertHookRecord2(getDb2(), {
13764
+ name: resolved.name,
13765
+ version: resolved.version,
13766
+ sha256: hash,
13767
+ source_type: resolved.source,
13768
+ last_verified_at: new Date().toISOString()
13769
+ });
13770
+ }
13611
13771
  results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
13612
13772
  continue;
13613
13773
  }
@@ -13885,8 +14045,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
13885
14045
  });
13886
14046
  var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
13887
14047
  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) => {
13888
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13889
- const db = getDb2();
14048
+ const db = await openLocalStoreOrExit(options.json);
13890
14049
  const limit = parseInt(options.limit) || 50;
13891
14050
  let sql = "SELECT * FROM hook_events WHERE 1=1";
13892
14051
  const params = [];
@@ -13923,8 +14082,7 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
13923
14082
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
13924
14083
  });
13925
14084
  logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
13926
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13927
- const db = getDb2();
14085
+ const db = await openLocalStoreOrExit(options.json);
13928
14086
  const limit = parseInt(options.limit) || 50;
13929
14087
  const q = `%${text}%`;
13930
14088
  const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
@@ -13949,8 +14107,7 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
13949
14107
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
13950
14108
  });
13951
14109
  logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
13952
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13953
- const db = getDb2();
14110
+ const db = await openLocalStoreOrExit(options.json);
13954
14111
  const limit = parseInt(options.n) || 20;
13955
14112
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
13956
14113
  const projected = projectLogRows2(rows);
@@ -13975,8 +14132,7 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
13975
14132
  Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
13976
14133
  });
13977
14134
  logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
13978
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13979
- const db = getDb2();
14135
+ const db = await openLocalStoreOrExit(options.json);
13980
14136
  const limit = parseInt(options.limit) || 50;
13981
14137
  function parseDuration(s) {
13982
14138
  const m = s.match(/^(\d+)(s|m|h|d)$/);
@@ -14018,8 +14174,7 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
14018
14174
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
14019
14175
  });
14020
14176
  logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
14021
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
14022
- const db = getDb2();
14177
+ const db = await openLocalStoreOrExit(false);
14023
14178
  const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
14024
14179
  const count = countRow?.n ?? 0;
14025
14180
  if (count === 0) {
@@ -14041,19 +14196,28 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
14041
14196
  });
14042
14197
  var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
14043
14198
  storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
14044
- const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14045
- const status = getStorageStatus2();
14046
- if (options.json) {
14047
- console.log(JSON.stringify(status, null, 2));
14048
- return;
14049
- }
14050
- console.log(chalk2.bold(`
14199
+ try {
14200
+ const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14201
+ const status = getStorageStatus2();
14202
+ if (options.json) {
14203
+ console.log(JSON.stringify(status, null, 2));
14204
+ return;
14205
+ }
14206
+ console.log(chalk2.bold(`
14051
14207
  Storage Status
14052
14208
  `));
14053
- console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
14054
- console.log(` Backend: ${status.backend}`);
14055
- console.log(` Tables: ${status.tables.join(", ")}`);
14056
- console.log(` Sync rows: ${status.sync.length}`);
14209
+ console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
14210
+ console.log(` Backend: ${status.backend}`);
14211
+ console.log(` Tables: ${status.tables.join(", ")}`);
14212
+ console.log(` Sync rows: ${status.sync.length}`);
14213
+ } catch (error) {
14214
+ const message = error instanceof Error ? error.message : String(error);
14215
+ if (options.json)
14216
+ console.log(JSON.stringify({ error: message }));
14217
+ else
14218
+ console.error(chalk2.red(`\u2717 ${message}`));
14219
+ process.exitCode = 1;
14220
+ }
14057
14221
  });
14058
14222
  storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
14059
14223
  try {
@@ -14263,6 +14427,13 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14263
14427
  const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14264
14428
  await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
14265
14429
  } else {
14430
+ const { decideHooksMcpAuthority: decideHooksMcpAuthority2 } = await Promise.resolve().then(() => (init_authority(), exports_authority));
14431
+ try {
14432
+ decideHooksMcpAuthority2();
14433
+ } catch (error) {
14434
+ console.error(error instanceof Error ? error.message : String(error));
14435
+ process.exit(1);
14436
+ }
14266
14437
  const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14267
14438
  const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
14268
14439
  const args = options.port ? ["--port", options.port] : [];
@@ -14271,7 +14442,6 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14271
14442
  });
14272
14443
  registerEventsCommands(program2, { source: "hooks" });
14273
14444
  var API_INDEPENDENT_COMMANDS = new Set([
14274
- "run",
14275
14445
  "serve",
14276
14446
  "mcp",
14277
14447
  "cf",
@@ -14283,11 +14453,11 @@ var API_INDEPENDENT_COMMANDS = new Set([
14283
14453
  "events"
14284
14454
  ]);
14285
14455
  function failClosedForMissingApiEnv(detail) {
14286
- console.error(chalk2.red(`hooks: no registry credential resolved and local mode is not explicitly enabled.
14456
+ console.error(chalk2.red(hooksFailClosedLine() + `
14287
14457
  ` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
14288
14458
  ` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
14289
- ` + `config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
14290
- ` + `HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
14459
+ config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
14460
+ HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
14291
14461
  ` + (detail ? `${detail}
14292
14462
  ` : "") + "Refusing to silently fall back to local storage."));
14293
14463
  process.exit(1);
@@ -14311,21 +14481,25 @@ function enforceTransportGate() {
14311
14481
  return;
14312
14482
  if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
14313
14483
  return;
14314
- if (hasHooksEnvAuthorityIntent(process.env))
14484
+ if (hasHooksEnvAuthorityIntent(process.env)) {
14485
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal("configured by HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY"));
14315
14486
  return;
14487
+ }
14316
14488
  if (isHooksLocalOptIn(process.env)) {
14317
14489
  announceHooksLocalMode();
14318
14490
  return;
14319
14491
  }
14492
+ let transport;
14320
14493
  try {
14321
- const transport = resolveHooksTransport(process.env);
14322
- if (transport.mode !== "remote" || !transport.authority) {
14323
- failClosedForMissingApiEnv();
14324
- }
14494
+ transport = resolveHooksTransport(process.env);
14325
14495
  } catch (error) {
14326
14496
  const message = error instanceof Error ? error.message : String(error);
14327
14497
  failClosedForMissingApiEnv(message);
14328
14498
  }
14499
+ if (transport.mode !== "remote" || !transport.authority) {
14500
+ failClosedForMissingApiEnv();
14501
+ }
14502
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
14329
14503
  }
14330
14504
  enforceTransportGate();
14331
14505
  program2.parse();