@hasna/hooks 0.9.4 → 0.9.6

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/dist/index.js CHANGED
@@ -4762,12 +4762,12 @@ __export(exports_store, {
4762
4762
  checkScriptHash: () => checkScriptHash,
4763
4763
  LockFileError: () => LockFileError
4764
4764
  });
4765
- import { createHash } from "crypto";
4766
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
4765
+ import { createHash as createHash2 } from "crypto";
4766
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
4767
4767
  import { readFile } from "fs/promises";
4768
4768
  import { randomBytes } from "crypto";
4769
4769
  function sha256Of(input) {
4770
- return createHash("sha256").update(input).digest("hex");
4770
+ return createHash2("sha256").update(input).digest("hex");
4771
4771
  }
4772
4772
  async function sha256File(path) {
4773
4773
  const buf = await readFile(path);
@@ -4828,11 +4828,11 @@ function assertLockShape(parsed, path) {
4828
4828
  }
4829
4829
  function readLock() {
4830
4830
  const path = getLockPath();
4831
- if (!existsSync6(path))
4831
+ if (!existsSync7(path))
4832
4832
  return { hooks: {} };
4833
4833
  let raw;
4834
4834
  try {
4835
- raw = readFileSync3(path, "utf-8");
4835
+ raw = readFileSync4(path, "utf-8");
4836
4836
  } catch {
4837
4837
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4838
4838
  }
@@ -4935,7 +4935,7 @@ async function verifyScriptHash(name, scriptPath) {
4935
4935
  return checkScriptHash(name, await sha256File(scriptPath));
4936
4936
  }
4937
4937
  function retrustHook(name, scriptPath, version, source) {
4938
- const actual = sha256Of(readFileSync3(scriptPath));
4938
+ const actual = sha256Of(readFileSync4(scriptPath));
4939
4939
  const db = storeDb();
4940
4940
  if (db) {
4941
4941
  upsertHookRecord(db, {
@@ -4974,12 +4974,12 @@ __export(exports_profiles, {
4974
4974
  deleteProfile: () => deleteProfile,
4975
4975
  createProfile: () => createProfile
4976
4976
  });
4977
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4978
- import { join as join8 } from "path";
4977
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4978
+ import { join as join9 } from "path";
4979
4979
  function getProfilesDir() {
4980
- const newDir = join8(getEffectiveDataRoot(), "profiles");
4981
- const oldDir = join8(getHomeDir(), ".hooks", "profiles");
4982
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
4980
+ const newDir = join9(getEffectiveDataRoot(), "profiles");
4981
+ const oldDir = join9(getHomeDir(), ".hooks", "profiles");
4982
+ if (!existsSync10(newDir) && existsSync10(oldDir)) {
4983
4983
  mkdirSync6(newDir, { recursive: true });
4984
4984
  cpSync2(oldDir, newDir, { recursive: true });
4985
4985
  }
@@ -4987,12 +4987,12 @@ function getProfilesDir() {
4987
4987
  }
4988
4988
  function ensureProfilesDir() {
4989
4989
  const dir = getProfilesDir();
4990
- if (!existsSync9(dir)) {
4990
+ if (!existsSync10(dir)) {
4991
4991
  mkdirSync6(dir, { recursive: true });
4992
4992
  }
4993
4993
  }
4994
4994
  function profilePath(id) {
4995
- return join8(getProfilesDir(), `${id}.json`);
4995
+ return join9(getProfilesDir(), `${id}.json`);
4996
4996
  }
4997
4997
  function shortUuid() {
4998
4998
  return crypto.randomUUID().slice(0, 8);
@@ -5018,23 +5018,23 @@ function createProfile(input) {
5018
5018
  function getProfile(id) {
5019
5019
  const path = profilePath(id);
5020
5020
  try {
5021
- if (!existsSync9(path))
5021
+ if (!existsSync10(path))
5022
5022
  return null;
5023
- return JSON.parse(readFileSync6(path, "utf-8"));
5023
+ return JSON.parse(readFileSync7(path, "utf-8"));
5024
5024
  } catch {
5025
5025
  return null;
5026
5026
  }
5027
5027
  }
5028
5028
  function listProfiles() {
5029
5029
  const dir = getProfilesDir();
5030
- if (!existsSync9(dir))
5030
+ if (!existsSync10(dir))
5031
5031
  return [];
5032
5032
  try {
5033
5033
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
5034
5034
  const profiles = [];
5035
5035
  for (const file of files) {
5036
5036
  try {
5037
- const content = readFileSync6(join8(dir, file), "utf-8");
5037
+ const content = readFileSync7(join9(dir, file), "utf-8");
5038
5038
  profiles.push(JSON.parse(content));
5039
5039
  } catch {}
5040
5040
  }
@@ -5057,7 +5057,7 @@ function updateProfile(id, data) {
5057
5057
  }
5058
5058
  function deleteProfile(id) {
5059
5059
  const path = profilePath(id);
5060
- if (!existsSync9(path))
5060
+ if (!existsSync10(path))
5061
5061
  return false;
5062
5062
  try {
5063
5063
  rmSync(path);
@@ -5087,7 +5087,7 @@ function importProfiles(profiles) {
5087
5087
  continue;
5088
5088
  }
5089
5089
  const path = profilePath(profile.agent_id);
5090
- if (existsSync9(path)) {
5090
+ if (existsSync10(path)) {
5091
5091
  skipped++;
5092
5092
  continue;
5093
5093
  }
@@ -5101,14 +5101,19 @@ var init_profiles = __esm(() => {
5101
5101
  init_app_home();
5102
5102
  });
5103
5103
 
5104
- // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
5104
+ // ../contracts/dist/client/transport.js
5105
5105
  import { isIP } from "net";
5106
5106
  import { spawnSync } from "child_process";
5107
- import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
5108
- import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5109
- import { createRequire } from "module";
5107
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
5108
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
5110
5109
  import { hostname as osHostname } from "os";
5111
- import { isAbsolute as isAbsolute2, join as join9 } from "path";
5110
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
5111
+ import { createRequire } from "module";
5112
+ import { fileURLToPath as fileURLToPath22 } from "url";
5113
+ import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
5114
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5115
+ import { basename as basename2, dirname as dirname6, join as join10, relative, sep as sep2 } from "path";
5116
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
5112
5117
  function envToken(name) {
5113
5118
  return name.toUpperCase().replace(/-/g, "_");
5114
5119
  }
@@ -5125,27 +5130,133 @@ function credentialOverrideEnvKey(name) {
5125
5130
  function credentialPointerEnvKey(name) {
5126
5131
  return `HASNA_${envToken(name)}_API_KEY_REF`;
5127
5132
  }
5133
+ function e(e2, n, r) {
5134
+ throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
5135
+ }
5136
+ function n(n2, i, o, f) {
5137
+ let s, u, l = r(n2, o), c = function(e2) {
5138
+ let n3 = new Set(["default", ...e2.conditions || []]);
5139
+ return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
5140
+ }(f || {}), a = i[l];
5141
+ if (a === undefined) {
5142
+ let e2, n3, r, t;
5143
+ for (t in i)
5144
+ n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
5145
+ a = i[n3];
5146
+ }
5147
+ return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
5148
+ let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
5149
+ for (;t < i2; t++)
5150
+ e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
5151
+ }(s, u), s;
5152
+ }
5153
+ function r(e2, n2, r2) {
5154
+ if (e2 === n2 || n2 === ".")
5155
+ return ".";
5156
+ let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
5157
+ return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
5158
+ }
5159
+ function t(e2, n2, r2) {
5160
+ if (e2) {
5161
+ if (typeof e2 == "string")
5162
+ return r2 && r2.add(e2), [e2];
5163
+ let i, o;
5164
+ if (Array.isArray(e2)) {
5165
+ for (o = r2 || new Set, i = 0;i < e2.length; i++)
5166
+ t(e2[i], n2, o);
5167
+ if (!r2 && o.size)
5168
+ return [...o];
5169
+ } else
5170
+ for (i in e2)
5171
+ if (n2.has(i))
5172
+ return t(e2[i], n2, r2);
5173
+ }
5174
+ }
5175
+ function o(e2, r2, t2) {
5176
+ let i, o2 = e2.exports;
5177
+ if (o2) {
5178
+ if (typeof o2 == "string")
5179
+ o2 = { ".": o2 };
5180
+ else
5181
+ for (i in o2) {
5182
+ i[0] !== "." && (o2 = { ".": o2 });
5183
+ break;
5184
+ }
5185
+ return n(e2.name, o2, r2 || ".", t2);
5186
+ }
5187
+ }
5188
+ function entryPoint(directory) {
5189
+ const root = realpathSync3(directory);
5190
+ const fd = openSync3(join10(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5191
+ let pkg;
5192
+ try {
5193
+ const stat = fstatSync3(fd);
5194
+ if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
5195
+ throw new Error("Invalid Secrets package metadata");
5196
+ pkg = JSON.parse(readFileSync8(fd, "utf8"));
5197
+ } finally {
5198
+ closeSync3(fd);
5199
+ }
5200
+ if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
5201
+ throw new Error("Invalid Secrets package identity");
5202
+ }
5203
+ const target = o(pkg, ".")?.[0];
5204
+ if (typeof target !== "string" || !target.startsWith("./"))
5205
+ throw new Error("Missing Secrets import export");
5206
+ const parts = target.slice(2).split("/");
5207
+ if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
5208
+ throw new Error("Invalid Secrets import export");
5209
+ }
5210
+ const file = realpathSync3(join10(root, ...parts));
5211
+ const within = relative(root, file);
5212
+ if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
5213
+ throw new Error("Secrets import export escapes its package or is not a file");
5214
+ }
5215
+ return pathToFileURL(file).href;
5216
+ }
5217
+ function resolveInstalledSecrets(parentUrl) {
5218
+ let directory = dirname6(fileURLToPath3(parentUrl));
5219
+ for (;; ) {
5220
+ if (basename2(directory) !== "node_modules") {
5221
+ const candidate = join10(directory, "node_modules", PACKAGE);
5222
+ let absent = false;
5223
+ try {
5224
+ lstatSync3(candidate);
5225
+ } catch (error) {
5226
+ if (error.code !== "ENOENT")
5227
+ throw error;
5228
+ absent = true;
5229
+ }
5230
+ if (!absent)
5231
+ return entryPoint(candidate);
5232
+ }
5233
+ const parent = dirname6(directory);
5234
+ if (parent === directory)
5235
+ throw new Error("Secrets SDK is not installed for this consumer");
5236
+ directory = parent;
5237
+ }
5238
+ }
5128
5239
  function homeDir(env) {
5129
5240
  const home = env.HOME?.trim();
5130
5241
  return home ? home : null;
5131
5242
  }
5132
5243
  function absoluteOverride(env, key) {
5133
5244
  const value = env[key]?.trim();
5134
- return value && isAbsolute2(value) ? value : null;
5245
+ return value && isAbsolute3(value) ? value : null;
5135
5246
  }
5136
5247
  function hasnaHomeDir(env) {
5137
5248
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
5138
5249
  if (override)
5139
5250
  return override;
5140
5251
  const home = homeDir(env);
5141
- return home ? join9(home, HASNA_HOME_DIR) : null;
5252
+ return home ? join22(home, HASNA_HOME_DIR) : null;
5142
5253
  }
5143
5254
  function appConfigDir(name, env) {
5144
5255
  const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
5145
5256
  if (configRoot)
5146
- return join9(configRoot, name);
5257
+ return join22(configRoot, name);
5147
5258
  const root = hasnaHomeDir(env);
5148
- return root ? join9(root, name, CONFIG_SUBDIR) : null;
5259
+ return root ? join22(root, name, CONFIG_SUBDIR) : null;
5149
5260
  }
5150
5261
  function credentialDiskSourceList(name, env, profile = null) {
5151
5262
  if (!SAFE_APP_SLUG.test(name))
@@ -5154,7 +5265,7 @@ function credentialDiskSourceList(name, env, profile = null) {
5154
5265
  if (!directory)
5155
5266
  return [];
5156
5267
  const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
5157
- return [{ path: join9(directory, file), tier: "disk" }];
5268
+ return [{ path: join22(directory, file), tier: "disk" }];
5158
5269
  }
5159
5270
  function credentialDiskSources(name, env) {
5160
5271
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -5177,9 +5288,9 @@ function parseEnvFile(text) {
5177
5288
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
5178
5289
  continue;
5179
5290
  let value = withoutExport.slice(equals + 1).trim();
5180
- const quote = value[0];
5181
- if (quote === '"' || quote === "'") {
5182
- if (value.length < 2 || !value.endsWith(quote)) {
5291
+ const quote2 = value[0];
5292
+ if (quote2 === '"' || quote2 === "'") {
5293
+ if (value.length < 2 || !value.endsWith(quote2)) {
5183
5294
  unusable.add(key);
5184
5295
  continue;
5185
5296
  }
@@ -5208,7 +5319,7 @@ function readAppConfigFile(path) {
5208
5319
  };
5209
5320
  let fd = -1;
5210
5321
  try {
5211
- fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5322
+ fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
5212
5323
  } catch (error) {
5213
5324
  const code = error.code;
5214
5325
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -5218,7 +5329,7 @@ function readAppConfigFile(path) {
5218
5329
  unsafe(`the path could not be opened (${code ?? "unknown error"})`);
5219
5330
  }
5220
5331
  try {
5221
- const before = fstatSync(fd);
5332
+ const before = fstatSync2(fd);
5222
5333
  if (!before.isFile())
5223
5334
  unsafe("the path is not a regular file");
5224
5335
  if (!configFileModeAllowed(before.mode)) {
@@ -5229,31 +5340,39 @@ function readAppConfigFile(path) {
5229
5340
  unsafe("the file is not owned by the current user");
5230
5341
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
5231
5342
  unsafe("the file exceeds the size limit");
5232
- const bytes = readFileSync7(fd);
5233
- const after = fstatSync(fd);
5343
+ const bytes = readFileSync22(fd);
5344
+ const after = fstatSync2(fd);
5234
5345
  if (!configFileReadsCoherent(before, after)) {
5235
5346
  unsafe("the file changed while being read");
5236
5347
  }
5237
5348
  return parseEnvFile(bytes.toString("utf8"));
5238
5349
  } finally {
5239
5350
  if (fd !== -1)
5240
- closeSync2(fd);
5351
+ closeSync22(fd);
5241
5352
  }
5242
5353
  }
5243
- function readCredentialFile(path, apiKeyKeys) {
5354
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
5244
5355
  const parsed = readAppConfigFile(path);
5245
5356
  if (!parsed)
5246
5357
  return null;
5247
- for (const key of apiKeyKeys) {
5358
+ for (const key of [...apiKeyKeys, pointerKey]) {
5248
5359
  if (parsed.unusable.has(key)) {
5249
5360
  throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
5250
5361
  }
5251
5362
  }
5252
5363
  const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
5364
+ const pointer = parsed.values.get(pointerKey)?.trim();
5365
+ if (pointer !== undefined) {
5366
+ if (!VAULT_POINTER_SHAPE.test(pointer))
5367
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
5368
+ if (values.length)
5369
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
5370
+ return { apiKey: "", pointerVaultKey: pointer };
5371
+ }
5253
5372
  if (new Set(values).size > 1) {
5254
5373
  throw new CredentialFileUnsafeError(path, "credential aliases disagree");
5255
5374
  }
5256
- return values[0] ?? null;
5375
+ return values[0] === undefined ? null : { apiKey: values[0] };
5257
5376
  }
5258
5377
  function appConfigDiskValue(name, env, keys) {
5259
5378
  const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
@@ -5325,6 +5444,18 @@ function sealCredential(fields) {
5325
5444
  });
5326
5445
  return Object.freeze(sealed);
5327
5446
  }
5447
+ function explicitCredential(appName, apiKey) {
5448
+ const source = "explicit apiKey option";
5449
+ assertUsableCredential(appName, source, apiKey);
5450
+ return sealCredential({
5451
+ apiKey,
5452
+ tier: "argument",
5453
+ source,
5454
+ deliberate: true,
5455
+ diskCandidates: [],
5456
+ warning: null
5457
+ });
5458
+ }
5328
5459
  function firstEnvValue(env, keys) {
5329
5460
  for (const key of keys) {
5330
5461
  if (!Object.prototype.hasOwnProperty.call(env, key))
@@ -5510,12 +5641,13 @@ function resolveCredential(name, env, options = {}) {
5510
5641
  }
5511
5642
  const paths = profileDiskSources(name, env, profile);
5512
5643
  for (const path of paths) {
5513
- const value = readCredentialFile(path, apiKeyKeys);
5644
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
5514
5645
  if (value) {
5515
- assertUsableCredential(name, path, value);
5646
+ if (!value.pointerVaultKey)
5647
+ assertUsableCredential(name, path, value.apiKey);
5516
5648
  return sealCredential({
5517
- apiKey: value,
5518
- tier: "profile",
5649
+ ...value,
5650
+ tier: value.pointerVaultKey ? "pointer" : "profile",
5519
5651
  source: path,
5520
5652
  deliberate: true,
5521
5653
  diskCandidates: paths,
@@ -5523,7 +5655,7 @@ function resolveCredential(name, env, options = {}) {
5523
5655
  });
5524
5656
  }
5525
5657
  }
5526
- 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);
5658
+ 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);
5527
5659
  }
5528
5660
  const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
5529
5661
  const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
@@ -5548,17 +5680,27 @@ function resolveCredential(name, env, options = {}) {
5548
5680
  });
5549
5681
  }
5550
5682
  const diskSourceList = credentialDiskSourceList(name, env, null);
5551
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
5683
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
5552
5684
  if (diskHits.length > 0) {
5553
5685
  const winner = diskHits[0];
5554
- assertUsableCredential(name, winner.src.path, winner.value);
5686
+ if (winner.value.pointerVaultKey) {
5687
+ return sealCredential({
5688
+ ...winner.value,
5689
+ tier: "pointer",
5690
+ source: winner.src.path,
5691
+ deliberate: false,
5692
+ diskCandidates: diskPaths,
5693
+ warning: null
5694
+ });
5695
+ }
5696
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
5555
5697
  const divergentSources = [
5556
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
5557
- ...envHit && envHit.value !== winner.value ? [envHit.key] : []
5698
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
5699
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
5558
5700
  ];
5559
5701
  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;
5560
5702
  return sealCredential({
5561
- apiKey: winner.value,
5703
+ apiKey: winner.value.apiKey,
5562
5704
  tier: winner.src.tier,
5563
5705
  source: winner.src.path,
5564
5706
  deliberate: false,
@@ -5579,6 +5721,53 @@ function resolveCredential(name, env, options = {}) {
5579
5721
  }
5580
5722
  return null;
5581
5723
  }
5724
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
5725
+ const secretsEnv = snapshotClientEnvironment("secrets", env);
5726
+ const vaultKey = pointerResolution.pointerVaultKey;
5727
+ const pointerEnvKey = pointerResolution.source;
5728
+ if (!vaultKey) {
5729
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
5730
+ }
5731
+ if (name === "secrets") {
5732
+ throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
5733
+ }
5734
+ let secretsSdk;
5735
+ try {
5736
+ const sdkUrl = resolveInstalledSecrets(import.meta.url);
5737
+ secretsSdk = requireSecretsSdk(fileURLToPath22(sdkUrl));
5738
+ } catch {
5739
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
5740
+ }
5741
+ let client;
5742
+ try {
5743
+ const bootstrap = resolveCredential("secrets", secretsEnv);
5744
+ if (!bootstrap || bootstrap.tier === "pointer") {
5745
+ throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
5746
+ }
5747
+ client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
5748
+ } catch {
5749
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
5750
+ }
5751
+ let secret;
5752
+ try {
5753
+ secret = await client.getSecret({ key: vaultKey });
5754
+ } catch {
5755
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
5756
+ }
5757
+ const value = secret.value;
5758
+ if (!value) {
5759
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
5760
+ }
5761
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
5762
+ return sealCredential({
5763
+ apiKey: value,
5764
+ tier: "pointer",
5765
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
5766
+ deliberate: pointerResolution.deliberate,
5767
+ diskCandidates: pointerResolution.diskCandidates,
5768
+ warning: null
5769
+ });
5770
+ }
5582
5771
  function defaultFleetGatewayBaseUrl(name) {
5583
5772
  return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
5584
5773
  }
@@ -5781,8 +5970,9 @@ function credentialDiskSourcesForMessage(name, env) {
5781
5970
  const paths = credentialDiskSources(name, env);
5782
5971
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
5783
5972
  }
5784
- 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;
5973
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", 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;
5785
5974
  var init_transport = __esm(() => {
5975
+ MAX_PACKAGE_BYTES = 1024 * 1024;
5786
5976
  CredentialResolutionError = class CredentialResolutionError extends Error {
5787
5977
  appName;
5788
5978
  attempted;
@@ -5810,7 +6000,6 @@ var init_transport = __esm(() => {
5810
6000
  INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
5811
6001
  CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
5812
6002
  AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
5813
- SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
5814
6003
  requireSecretsSdk = createRequire(import.meta.url);
5815
6004
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
5816
6005
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -6013,12 +6202,12 @@ var init_db_writer = __esm(() => {
6013
6202
  });
6014
6203
 
6015
6204
  // src/lib/hook-env.ts
6016
- import { dirname as dirname6, isAbsolute as isAbsolute3 } from "path";
6017
- import { existsSync as existsSync11, realpathSync as realpathSync2, statSync } from "fs";
6205
+ import { dirname as dirname8, isAbsolute as isAbsolute4 } from "path";
6206
+ import { existsSync as existsSync12, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
6018
6207
  import { tmpdir as tmpdir2 } from "os";
6019
6208
  function isDeniedEnvName(name) {
6020
6209
  const upper = name.toUpperCase();
6021
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
6210
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
6022
6211
  return true;
6023
6212
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
6024
6213
  return true;
@@ -6027,13 +6216,13 @@ function isDeniedEnvName(name) {
6027
6216
  function isInterpreterInjectionEnvName(name) {
6028
6217
  if (INTERPRETER_INJECTION.has(name))
6029
6218
  return true;
6030
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
6219
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
6031
6220
  }
6032
6221
  function isBashFunctionEnvName(name) {
6033
6222
  return name.startsWith("BASH_FUNC_");
6034
6223
  }
6035
6224
  function isUnsafePathEntry(entry, home) {
6036
- if (entry === "" || !isAbsolute3(entry))
6225
+ if (entry === "" || !isAbsolute4(entry))
6037
6226
  return true;
6038
6227
  if (home && (entry === home || entry.startsWith(home + "/")))
6039
6228
  return true;
@@ -6043,7 +6232,7 @@ function isUnsafePathEntry(entry, home) {
6043
6232
  return true;
6044
6233
  const canonical = (path) => {
6045
6234
  try {
6046
- return realpathSync2(path);
6235
+ return realpathSync4(path);
6047
6236
  } catch {
6048
6237
  return path;
6049
6238
  }
@@ -6055,16 +6244,16 @@ function isUnsafePathEntry(entry, home) {
6055
6244
  return true;
6056
6245
  }
6057
6246
  try {
6058
- if ((statSync(entry).mode & 2) !== 0)
6247
+ if ((statSync2(entry).mode & 2) !== 0)
6059
6248
  return true;
6060
6249
  } catch {}
6061
6250
  return false;
6062
6251
  }
6063
6252
  function sanitizeHookPath(pathValue, home) {
6064
6253
  const baseline = PATH_BASELINE.slice();
6065
- if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
6254
+ if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
6066
6255
  baseline.push("/opt/homebrew/bin");
6067
- const bunDir = dirname6(process.execPath);
6256
+ const bunDir = dirname8(process.execPath);
6068
6257
  const seen = new Set;
6069
6258
  const out = [];
6070
6259
  for (const dir of baseline) {
@@ -6189,9 +6378,9 @@ __export(exports_run, {
6189
6378
  });
6190
6379
  import { randomBytes as randomBytes2 } from "crypto";
6191
6380
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
6192
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute4, join as join12 } from "path";
6381
+ import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute5, join as join13 } from "path";
6193
6382
  function extensionOf(scriptPath) {
6194
- const base = basename2(scriptPath);
6383
+ const base = basename3(scriptPath);
6195
6384
  const dot = base.lastIndexOf(".");
6196
6385
  return dot > 0 ? base.slice(dot + 1) : "ts";
6197
6386
  }
@@ -6203,9 +6392,9 @@ function shebangInterpreter(firstLine) {
6203
6392
  return null;
6204
6393
  const parts = rest.split(/\s+/);
6205
6394
  if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
6206
- return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
6395
+ return parts[1] && parts[1] !== "-S" ? basename3(parts[1]) : parts[2] ? basename3(parts[2]) : null;
6207
6396
  }
6208
- return basename2(parts[0]);
6397
+ return basename3(parts[0]);
6209
6398
  }
6210
6399
  function interpreterFor(name, scriptPath, content) {
6211
6400
  const ext = extensionOf(scriptPath);
@@ -6239,8 +6428,8 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6239
6428
  result = await Promise.race([
6240
6429
  reader.read(),
6241
6430
  new Promise((_, reject) => {
6242
- const t = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
6243
- t.unref?.();
6431
+ const t2 = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
6432
+ t2.unref?.();
6244
6433
  })
6245
6434
  ]);
6246
6435
  } catch {
@@ -6258,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6258
6447
  return chunks.join("");
6259
6448
  }
6260
6449
  async function executeVerifiedScript(options) {
6261
- const scriptDir = dirname7(options.scriptPath);
6450
+ const scriptDir = dirname9(options.scriptPath);
6262
6451
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6263
- const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6452
+ const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6264
6453
  let proc = null;
6265
6454
  let timedOut = false;
6266
6455
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -6269,7 +6458,7 @@ async function executeVerifiedScript(options) {
6269
6458
  }, options.timeout) : null;
6270
6459
  try {
6271
6460
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
6272
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6461
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6273
6462
  stdin: new Response(options.stdin),
6274
6463
  stdout: "pipe",
6275
6464
  stderr: "pipe",
@@ -6845,6 +7034,7 @@ var HOOKS = [
6845
7034
  },
6846
7035
  {
6847
7036
  name: "workspace-repos-guard",
7037
+ timeoutSeconds: 10,
6848
7038
  displayName: "Workspace Repos Guard",
6849
7039
  description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
6850
7040
  version: "0.1.0",
@@ -6863,7 +7053,7 @@ var HOOKS = [
6863
7053
  matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
6864
7054
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
6865
7055
  rewritesInput: true,
6866
- timeoutSeconds: 5
7056
+ timeoutSeconds: 10
6867
7057
  },
6868
7058
  {
6869
7059
  name: "checktests",
@@ -7352,9 +7542,9 @@ function getHook(name) {
7352
7542
  return HOOKS.find((h) => h.name === name);
7353
7543
  }
7354
7544
  // src/lib/installer.ts
7355
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7356
- import { join as join7, dirname as dirname4 } from "path";
7357
- import { homedir as homedir3 } from "os";
7545
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7546
+ import { join as join8, dirname as dirname5 } from "path";
7547
+ import { homedir as homedir4 } from "os";
7358
7548
  import { fileURLToPath as fileURLToPath2 } from "url";
7359
7549
 
7360
7550
  // src/lib/resolve.ts
@@ -7445,6 +7635,137 @@ function resolveHook(name) {
7445
7635
  // src/lib/installer.ts
7446
7636
  init_manifest();
7447
7637
 
7638
+ // src/lib/native-safety.ts
7639
+ import { createHash } from "crypto";
7640
+ import { closeSync, constants, existsSync as existsSync6, fstatSync, lstatSync, openSync, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
7641
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "path";
7642
+ import { homedir as homedir3 } from "os";
7643
+ function isNativeSafetyName(name) {
7644
+ return name === "trash-guard" || name === "workspace-repos-guard";
7645
+ }
7646
+ var supervisorSource = String.raw`async function supervise(encoded) {
7647
+ const fs = require("node:fs");
7648
+ const path = require("node:path");
7649
+ const crypto = require("node:crypto");
7650
+ const cp = require("node:child_process");
7651
+ const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
7652
+ try {
7653
+ const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
7654
+ const check = (file, hash, maximum) => {
7655
+ if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
7656
+ for (let parent = path.dirname(file);;) {
7657
+ const s = fs.lstatSync(parent);
7658
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
7659
+ if (parent === path.dirname(parent)) break;
7660
+ parent = path.dirname(parent);
7661
+ }
7662
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
7663
+ try {
7664
+ const s = fs.fstatSync(fd);
7665
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
7666
+ const bytes = fs.readFileSync(fd);
7667
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
7668
+ return bytes;
7669
+ } finally { fs.closeSync(fd); }
7670
+ };
7671
+ check(b.runtime, b.runtimeSHA256, 200_000_000);
7672
+ const bytes = check(b.worker, b.workerSHA256, 256_000);
7673
+ const deadline = setTimeout(fail, 1500);
7674
+ const chunks = []; let size = 0;
7675
+ for await (const chunk of process.stdin) {
7676
+ size += chunk.length;
7677
+ if (size > 1_048_576) throw Error();
7678
+ chunks.push(chunk);
7679
+ }
7680
+ clearTimeout(deadline);
7681
+ const input = Buffer.concat(chunks);
7682
+ const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
7683
+ input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
7684
+ });
7685
+ if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
7686
+ const result = JSON.parse(child.stdout.toString("utf8"));
7687
+ if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
7688
+ if (result.verdict === null) return;
7689
+ const output = result.verdict?.hookSpecificOutput;
7690
+ if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
7691
+ if (output.permissionDecision === "deny") {
7692
+ if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
7693
+ } else if (output.permissionDecision === "allow") {
7694
+ if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
7695
+ } else throw Error();
7696
+ process.stdout.write(JSON.stringify(result.verdict));
7697
+ } catch { fail(); }
7698
+ }`;
7699
+ var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
7700
+ var program = `(${supervisorSource})(process.argv[1]);`;
7701
+ var shell = `cd / || exit 2
7702
+ exec 3<&0 || exit 2
7703
+ /usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
7704
+ runner=$!
7705
+ exec 3<&-
7706
+ (trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
7707
+ watchdog=$!
7708
+ wait "$runner"; result=$?
7709
+ kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
7710
+ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
7711
+ var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
7712
+ function validateBinding(b) {
7713
+ if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
7714
+ throw new Error("Invalid native safety binding");
7715
+ }
7716
+ function buildNativeSafetyCommand(binding) {
7717
+ validateBinding(binding);
7718
+ const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
7719
+ return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
7720
+ }
7721
+ function nativeSafetyRegistration(command) {
7722
+ if (!command.startsWith(prefix))
7723
+ return;
7724
+ const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
7725
+ if (!encoded || encoded.length > 16384)
7726
+ return;
7727
+ try {
7728
+ const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
7729
+ return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
7730
+ } catch {
7731
+ return;
7732
+ }
7733
+ }
7734
+ function pinEligibleFile(file, maximum, executable = false) {
7735
+ if (!isAbsolute2(file) || realpathSync2(file) !== file)
7736
+ throw new Error("Native safety requires canonical files");
7737
+ for (let parent = dirname3(file);; ) {
7738
+ const s = lstatSync(parent);
7739
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
7740
+ throw new Error("Native safety requires trusted, non-writable file ancestry");
7741
+ if (parent === dirname3(parent))
7742
+ break;
7743
+ parent = dirname3(parent);
7744
+ }
7745
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
7746
+ try {
7747
+ const s = fstatSync(fd);
7748
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
7749
+ throw new Error("Native safety requires an eligible installed file");
7750
+ const bytes = readFileSync3(fd);
7751
+ if (bytes.length > maximum)
7752
+ throw new Error("Native safety file exceeds its bound");
7753
+ return createHash("sha256").update(bytes).digest("hex");
7754
+ } finally {
7755
+ closeSync(fd);
7756
+ }
7757
+ }
7758
+ function installedNativeSafetyCommand(name) {
7759
+ const home = realpathSync2(homedir3());
7760
+ const runtime = realpathSync2(process.execPath);
7761
+ const candidates = [join7(import.meta.dir, "../../bin/native-safety-entry.js"), join7(import.meta.dir, "../bin/native-safety-entry.js"), join7(import.meta.dir, "native-safety-entry.js")];
7762
+ const worker = candidates.find(existsSync6);
7763
+ if (!worker)
7764
+ throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
7765
+ const canonicalWorker = realpathSync2(worker);
7766
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join7(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
7767
+ }
7768
+
7448
7769
  // src/lib/mementos-options.ts
7449
7770
  function validateMementosRegistration(name, value) {
7450
7771
  if (value === undefined)
@@ -7506,6 +7827,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
7506
7827
  function registrationName(command) {
7507
7828
  if (typeof command !== "string")
7508
7829
  return;
7830
+ const native = nativeSafetyRegistration(command);
7831
+ if (native)
7832
+ return native;
7509
7833
  if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
7510
7834
  try {
7511
7835
  retainedMementosRegistration([command.trim()]);
@@ -7525,16 +7849,16 @@ function matchersOverlap(a, b) {
7525
7849
  init_store();
7526
7850
 
7527
7851
  // src/lib/codex-settings.ts
7528
- import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7529
- import { dirname as dirname3 } from "path";
7852
+ import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7853
+ import { dirname as dirname4 } from "path";
7530
7854
  import { randomUUID } from "crypto";
7531
7855
  var snapshots = new WeakMap;
7532
7856
  function current(path) {
7533
7857
  try {
7534
- const stat = lstatSync(path);
7858
+ const stat = lstatSync2(path);
7535
7859
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
7536
7860
  throw new Error("invalid_file");
7537
- return readFileSync4(path, "utf8");
7861
+ return readFileSync5(path, "utf8");
7538
7862
  } catch (error) {
7539
7863
  if (error.code === "ENOENT")
7540
7864
  return null;
@@ -7566,11 +7890,11 @@ function writeCodexSettings(path, settings) {
7566
7890
  const snapshot = snapshots.get(settings);
7567
7891
  if (!snapshot || snapshot.path !== path)
7568
7892
  throw new Error("Codex settings require a complete current read before writing.");
7569
- mkdirSync4(dirname3(path), { recursive: true });
7893
+ mkdirSync4(dirname4(path), { recursive: true });
7570
7894
  const lock = `${path}.hooks-write.lock`;
7571
7895
  let fd;
7572
7896
  try {
7573
- fd = openSync(lock, "wx", 384);
7897
+ fd = openSync2(lock, "wx", 384);
7574
7898
  } catch {
7575
7899
  throw new Error("Codex settings are being edited; retry after the active writer finishes.");
7576
7900
  }
@@ -7586,16 +7910,16 @@ function writeCodexSettings(path, settings) {
7586
7910
  renameSync2(temp, path);
7587
7911
  snapshots.set(settings, { path, raw: next });
7588
7912
  } finally {
7589
- if (existsSync7(temp))
7913
+ if (existsSync8(temp))
7590
7914
  unlinkSync(temp);
7591
- closeSync(fd);
7915
+ closeSync2(fd);
7592
7916
  unlinkSync(lock);
7593
7917
  }
7594
7918
  }
7595
7919
 
7596
7920
  // src/lib/installer.ts
7597
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
7598
- var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
7921
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
7922
+ var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
7599
7923
  function normalizeHookName(name) {
7600
7924
  return name.startsWith("hook-") ? name : `hook-${name}`;
7601
7925
  }
@@ -7607,8 +7931,7 @@ function removeHookEntriesByName(entries, hookName) {
7607
7931
  if (!Array.isArray(entry.hooks))
7608
7932
  return [entry];
7609
7933
  const kept = entry.hooks.filter((h) => {
7610
- const match = h.command?.match(/^hooks run ([\w-]+)/);
7611
- return !(match && match[1] === hookName);
7934
+ return registrationName(h.command) !== hookName;
7612
7935
  });
7613
7936
  return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
7614
7937
  });
@@ -7684,29 +8007,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
7684
8007
  return globalOverride;
7685
8008
  const dir = getTargetSettingsDir(target);
7686
8009
  if (target === "codex")
7687
- return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
8010
+ return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
7688
8011
  if (scope === "project") {
7689
- return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
8012
+ return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
7690
8013
  }
7691
- return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
8014
+ return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
7692
8015
  }
7693
8016
  function getHookPath(name) {
7694
8017
  const shortName = shortHookName(name);
7695
8018
  const resolved = resolveHookDir(shortName);
7696
8019
  if (resolved)
7697
8020
  return resolved;
7698
- return join7(HOOKS_DIR, normalizeHookName(shortName));
8021
+ return join8(HOOKS_DIR, normalizeHookName(shortName));
7699
8022
  }
7700
8023
  function hookExists(name) {
7701
- return existsSync8(getHookPath(name));
8024
+ return existsSync9(getHookPath(name));
7702
8025
  }
7703
8026
  function readSettings(scope = "global", target = "claude") {
7704
8027
  const path = getSettingsPath(scope, target);
7705
8028
  if (target === "codex")
7706
8029
  return readCodexSettings(path);
7707
8030
  try {
7708
- if (existsSync8(path)) {
7709
- return JSON.parse(readFileSync5(path, "utf-8"));
8031
+ if (existsSync9(path)) {
8032
+ return JSON.parse(readFileSync6(path, "utf-8"));
7710
8033
  }
7711
8034
  } catch (error) {
7712
8035
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -7717,8 +8040,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
7717
8040
  const path = getSettingsPath(scope, target);
7718
8041
  if (target === "codex")
7719
8042
  return writeCodexSettings(path, settings);
7720
- const dir = dirname4(path);
7721
- if (!existsSync8(dir)) {
8043
+ const dir = dirname5(path);
8044
+ if (!existsSync9(dir)) {
7722
8045
  mkdirSync5(dir, { recursive: true });
7723
8046
  }
7724
8047
  writeFileSync4(path, JSON.stringify(settings, null, 2) + `
@@ -7746,6 +8069,9 @@ function codewithTimeout(name) {
7746
8069
  switch (name) {
7747
8070
  case "mementos-context":
7748
8071
  return 12;
8072
+ case "trash-guard":
8073
+ case "workspace-repos-guard":
8074
+ return 10;
7749
8075
  case "knowledge-context":
7750
8076
  return 6;
7751
8077
  case "session-start":
@@ -7755,7 +8081,6 @@ function codewithTimeout(name) {
7755
8081
  case "prompt-guard":
7756
8082
  return 3;
7757
8083
  case "worktree-guard":
7758
- case "trash-guard":
7759
8084
  case "stop-sync":
7760
8085
  return 5;
7761
8086
  default:
@@ -7784,11 +8109,13 @@ function codewithStatusMessage(name) {
7784
8109
  }
7785
8110
  function buildCodewithTomlFragment(name, profile, mementos) {
7786
8111
  const shortName = shortHookName(name);
7787
- const meta = resolveHookMeta(shortName);
8112
+ if (isNativeSafetyName(shortName) && profile)
8113
+ throw new Error("Bundled native safety guards do not select registry profiles");
8114
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
7788
8115
  if (!meta)
7789
8116
  throw new Error(`Hook '${shortName}' not found`);
7790
8117
  validateMementosRegistration(shortName, mementos);
7791
- const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
8118
+ const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
7792
8119
  const matcher = codewithMatcher(meta.matcher);
7793
8120
  const fragments = [];
7794
8121
  for (const event of getHookEvents(meta)) {
@@ -7813,17 +8140,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
7813
8140
  function readCodewithConfig(scope, configPath) {
7814
8141
  const path = getSettingsPath(scope, "codewith", configPath);
7815
8142
  try {
7816
- return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
8143
+ return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
7817
8144
  } catch {
7818
8145
  return "";
7819
8146
  }
7820
8147
  }
7821
8148
  function appendCodewithFragment(fragment, scope, configPath) {
7822
8149
  const path = getSettingsPath(scope, "codewith", configPath);
7823
- const dir = dirname4(path);
7824
- if (!existsSync8(dir))
8150
+ const dir = dirname5(path);
8151
+ if (!existsSync9(dir))
7825
8152
  mkdirSync5(dir, { recursive: true });
7826
- const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
8153
+ const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
7827
8154
  const sep2 = existing.trim() ? `
7828
8155
 
7829
8156
  ` : "";
@@ -7831,7 +8158,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
7831
8158
  return path;
7832
8159
  }
7833
8160
  function detectConflict(name, scope, target) {
7834
- const meta = resolveHookMeta(name);
8161
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
7835
8162
  if (!meta || !meta.matcher)
7836
8163
  return;
7837
8164
  const events = new Set(getHookEvents(meta));
@@ -7851,7 +8178,7 @@ function detectConflict(name, scope, target) {
7851
8178
  return;
7852
8179
  }
7853
8180
  function detectRewriteConflict(name, scope, target, lookup = getHook) {
7854
- const meta = resolveHookMeta(name);
8181
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
7855
8182
  if (!meta?.matcher || !meta.rewritesInput)
7856
8183
  return;
7857
8184
  if (!getHookEvents(meta).includes("PreToolUse"))
@@ -7872,7 +8199,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
7872
8199
  }
7873
8200
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
7874
8201
  const shortName = shortHookName(name);
7875
- if (!hookExists(shortName)) {
8202
+ if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
8203
+ return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
8204
+ }
8205
+ if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
7876
8206
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
7877
8207
  }
7878
8208
  if (target === "codewith") {
@@ -7964,7 +8294,7 @@ function installHook(name, options = {}) {
7964
8294
  }
7965
8295
  if (target === "all") {
7966
8296
  const shortName = shortHookName(name);
7967
- const meta = resolveHookMeta(shortName);
8297
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
7968
8298
  if (meta) {
7969
8299
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
7970
8300
  if (unsupportedTargets.length > 0) {
@@ -7997,7 +8327,7 @@ function installHook(name, options = {}) {
7997
8327
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
7998
8328
  }
7999
8329
  function registerHook(name, scope = "global", target = "claude", profile, mementos) {
8000
- const meta = resolveHookMeta(name);
8330
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
8001
8331
  if (!meta)
8002
8332
  return;
8003
8333
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -8014,8 +8344,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
8014
8344
  const settings = readSettings(scope, target);
8015
8345
  if (!settings.hooks)
8016
8346
  settings.hooks = {};
8347
+ if (isNativeSafetyName(name)) {
8348
+ for (const groups of Object.values(settings.hooks))
8349
+ for (const group of groups)
8350
+ for (const hook of group.hooks ?? []) {
8351
+ if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
8352
+ throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
8353
+ }
8354
+ }
8355
+ }
8017
8356
  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;
8018
- const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
8357
+ const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
8019
8358
  if (target === "codex") {
8020
8359
  const positions = [];
8021
8360
  for (const [event, groups] of Object.entries(settings.hooks)) {
@@ -8023,9 +8362,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
8023
8362
  for (let index = 0;index < entry.hooks.length; index++) {
8024
8363
  const command = entry.hooks[index]?.command;
8025
8364
  const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
8026
- if (match && (match[1] ?? match[2]) === name) {
8365
+ if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
8027
8366
  if (registrationName(command) !== name)
8028
8367
  throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
8368
+ if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
8369
+ throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
8029
8370
  positions.push({ event, entry, index });
8030
8371
  }
8031
8372
  }
@@ -8089,12 +8430,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
8089
8430
  if (target === "codewith") {
8090
8431
  const config = readCodewithConfig(scope);
8091
8432
  const registered2 = [];
8092
- const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
8093
- let match;
8094
- while (match = re.exec(config)) {
8095
- const name = registrationName(match[1]);
8096
- if (name)
8097
- registered2.push(name);
8433
+ try {
8434
+ const parsed = Bun.TOML.parse(config);
8435
+ for (const groups of Object.values(parsed.hooks ?? {})) {
8436
+ if (!Array.isArray(groups))
8437
+ continue;
8438
+ for (const group of groups)
8439
+ for (const hook of group.hooks ?? []) {
8440
+ const name = registrationName(hook.command);
8441
+ if (name)
8442
+ registered2.push(name);
8443
+ }
8444
+ }
8445
+ } catch {
8446
+ return [];
8098
8447
  }
8099
8448
  return [...new Set(registered2)];
8100
8449
  }
@@ -8195,10 +8544,10 @@ function codewithHasHookEntryInText(configText, name) {
8195
8544
  }
8196
8545
  function codewithHasHookEntry(name, scope = "global") {
8197
8546
  const path = getSettingsPath(scope, "codewith");
8198
- if (!existsSync8(path))
8547
+ if (!existsSync9(path))
8199
8548
  return false;
8200
8549
  try {
8201
- return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
8550
+ return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
8202
8551
  } catch {
8203
8552
  return false;
8204
8553
  }
@@ -8207,12 +8556,12 @@ function removeStoreDirFailClosed(dir) {
8207
8556
  const entries = readdirSync3(dir, { withFileTypes: true });
8208
8557
  for (const entry of entries) {
8209
8558
  if (entry.isDirectory()) {
8210
- removeStoreDirFailClosed(join7(dir, entry.name));
8559
+ removeStoreDirFailClosed(join8(dir, entry.name));
8211
8560
  }
8212
8561
  }
8213
8562
  for (const entry of entries) {
8214
8563
  if (!entry.isDirectory()) {
8215
- unlinkSync2(join7(dir, entry.name));
8564
+ unlinkSync2(join8(dir, entry.name));
8216
8565
  }
8217
8566
  }
8218
8567
  rmdirSync(dir);
@@ -8243,8 +8592,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
8243
8592
  const registrationsRemaining = [];
8244
8593
  if (target === "codewith" || target === "all") {
8245
8594
  const codewithPath = getSettingsPath(scope, "codewith");
8246
- if (existsSync8(codewithPath)) {
8247
- const before = readFileSync5(codewithPath, "utf-8");
8595
+ if (existsSync9(codewithPath)) {
8596
+ const before = readFileSync6(codewithPath, "utf-8");
8248
8597
  const after = removeCodewithHookEntry(before, shortName);
8249
8598
  if (after.removed) {
8250
8599
  writeFileSync4(codewithPath, after.text, "utf-8");
@@ -8286,7 +8635,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
8286
8635
  // src/index.ts
8287
8636
  init_profiles();
8288
8637
  init_config();
8289
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
8638
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
8290
8639
 
8291
8640
  // src/lib/transport.ts
8292
8641
  init_transport();
@@ -8321,6 +8670,41 @@ function rethrowHooksAuthorityFailure(error) {
8321
8670
  }
8322
8671
  throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
8323
8672
  }
8673
+ var authorityBindings = new WeakMap;
8674
+ function sameCredential(left, right) {
8675
+ return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
8676
+ }
8677
+ function changedBinding() {
8678
+ throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
8679
+ }
8680
+ async function resolveHooksRequestAuthority(authority) {
8681
+ if (authority.origin !== hooksRegistryOrigin(authority.v1BaseUrl))
8682
+ changedBinding();
8683
+ const binding = authorityBindings.get(authority);
8684
+ if (!binding)
8685
+ return { ...authority, apiKey: explicitCredential("hooks", authority.apiKey).apiKey };
8686
+ const snapshot = () => {
8687
+ const next = resolveHooksTransport(binding.env, binding.options);
8688
+ if (next.mode !== "remote" || !next.authority || next.authority.v1BaseUrl !== authority.v1BaseUrl)
8689
+ changedBinding();
8690
+ return { authority: next.authority, binding: authorityBindings.get(next.authority) };
8691
+ };
8692
+ const first = snapshot();
8693
+ const reviewed = snapshot();
8694
+ if (!sameCredential(first.binding.credential, reviewed.binding.credential))
8695
+ changedBinding();
8696
+ let credential = reviewed.binding.credential;
8697
+ try {
8698
+ if (credential.tier === "pointer")
8699
+ credential = await completePointerCredential("hooks", credential, binding.env);
8700
+ } catch (error) {
8701
+ rethrowHooksAuthorityFailure(error);
8702
+ }
8703
+ const final = snapshot();
8704
+ if (!sameCredential(reviewed.binding.credential, final.binding.credential))
8705
+ changedBinding();
8706
+ return { ...reviewed.authority, apiKey: credential.apiKey };
8707
+ }
8324
8708
  function resolveHooksTransport(env = process.env, options = {}) {
8325
8709
  if (selectsHooksLocalStore(env)) {
8326
8710
  announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
@@ -8334,7 +8718,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
8334
8718
  try {
8335
8719
  credential = resolveCredential("hooks", resolverEnv, credentialOptions);
8336
8720
  resolution = resolveClientTransport("hooks", resolverEnv, {
8337
- credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
8721
+ credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
8338
8722
  });
8339
8723
  } catch (error) {
8340
8724
  rethrowHooksAuthorityFailure(error);
@@ -8342,25 +8726,43 @@ function resolveHooksTransport(env = process.env, options = {}) {
8342
8726
  if (!credential) {
8343
8727
  throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
8344
8728
  }
8729
+ const authority = {
8730
+ origin: hooksRegistryOrigin(resolution.baseUrl),
8731
+ apiKey: credential.apiKey,
8732
+ apiUrlSource: resolution.apiUrlSource,
8733
+ apiKeySource: credential.source,
8734
+ apiKeyTier: credential.tier,
8735
+ v1BaseUrl: resolution.baseUrl,
8736
+ warning: resolution.warning
8737
+ };
8738
+ authorityBindings.set(authority, { env, options, credential });
8345
8739
  return {
8346
8740
  mode: "remote",
8347
8741
  source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
8348
- authority: {
8349
- origin: hooksRegistryOrigin(resolution.baseUrl),
8350
- apiKey: credential.apiKey,
8351
- apiUrlSource: resolution.apiUrlSource,
8352
- apiKeySource: credential.source,
8353
- apiKeyTier: credential.tier,
8354
- v1BaseUrl: resolution.baseUrl,
8355
- warning: resolution.warning
8356
- }
8742
+ authority
8357
8743
  };
8358
8744
  }
8359
8745
  function resolveHooksServePublishKey(env = process.env, options = {}) {
8360
8746
  const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8361
8747
  const tier1 = options.credentials?.apiKey;
8362
8748
  const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
8363
- return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
8749
+ const credential = resolveCredential("hooks", resolverEnv, credentialOptions);
8750
+ if (credential?.tier === "pointer")
8751
+ throw new Error("Vault references require resolveHooksServePublishKeyAsync().");
8752
+ return credential?.apiKey;
8753
+ }
8754
+ async function resolveHooksServePublishKeyAsync(env = process.env, options = {}) {
8755
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8756
+ const read = () => resolveCredential("hooks", resolverEnv, credentials);
8757
+ const selected = read();
8758
+ if (!selected || selected.tier !== "pointer")
8759
+ return selected?.apiKey;
8760
+ if (!sameCredential(selected, read()))
8761
+ changedBinding();
8762
+ const completed = await completePointerCredential("hooks", selected, env);
8763
+ if (!sameCredential(selected, read()))
8764
+ changedBinding();
8765
+ return completed.apiKey;
8364
8766
  }
8365
8767
 
8366
8768
  // src/index.ts
@@ -8373,15 +8775,15 @@ init_manifest();
8373
8775
  // src/lib/custom-install.ts
8374
8776
  init_config();
8375
8777
  init_manifest();
8376
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "fs";
8377
- import { join as join10 } from "path";
8778
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync2 } from "fs";
8779
+ import { join as join11 } from "path";
8378
8780
  import { tmpdir } from "os";
8379
8781
  function manifestAt(dir) {
8380
- const path = join10(dir, "manifest.json");
8381
- if (!existsSync10(path))
8782
+ const path = join11(dir, "manifest.json");
8783
+ if (!existsSync11(path))
8382
8784
  return;
8383
8785
  try {
8384
- return parseManifest(readFileSync8(path, "utf-8"));
8786
+ return parseManifest(readFileSync9(path, "utf-8"));
8385
8787
  } catch {
8386
8788
  return;
8387
8789
  }
@@ -8400,8 +8802,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
8400
8802
  return { name, version: manifest.version, kind, source, dir, scriptPath };
8401
8803
  }
8402
8804
  async function cloneToTemp(gitUrl) {
8403
- const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
8404
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
8805
+ const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
8806
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
8405
8807
  stdout: "pipe",
8406
8808
  stderr: "pipe"
8407
8809
  });
@@ -8414,7 +8816,7 @@ async function cloneToTemp(gitUrl) {
8414
8816
  rmSync2(tmp, { recursive: true, force: true });
8415
8817
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
8416
8818
  }
8417
- return join10(tmp, "repo");
8819
+ return join11(tmp, "repo");
8418
8820
  }
8419
8821
  function isGitUrl(value) {
8420
8822
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -8424,7 +8826,7 @@ function isCustomSource(value) {
8424
8826
  return true;
8425
8827
  if (value.startsWith("http://") || value.startsWith("https://"))
8426
8828
  return true;
8427
- return existsSync10(value);
8829
+ return existsSync11(value);
8428
8830
  }
8429
8831
  async function installCustomSource(source) {
8430
8832
  if (isGitUrl(source)) {
@@ -8438,11 +8840,11 @@ async function installCustomSource(source) {
8438
8840
  if (source.startsWith("http://") || source.startsWith("https://")) {
8439
8841
  return installManifestUrl(source);
8440
8842
  }
8441
- if (!existsSync10(source)) {
8843
+ if (!existsSync11(source)) {
8442
8844
  throw new Error(`No such local path: ${source}`);
8443
8845
  }
8444
8846
  if (source.endsWith("manifest.json")) {
8445
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8847
+ const manifest = parseManifest(readFileSync9(source, "utf-8"));
8446
8848
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
8447
8849
  }
8448
8850
  return copyManifestDir(source, source, "local");
@@ -8472,16 +8874,16 @@ async function installManifestUrl(url) {
8472
8874
  return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
8473
8875
  }
8474
8876
  // src/lib/sync.ts
8475
- import { createHash as createHash2 } from "crypto";
8476
- import { readFileSync as readFileSync9 } from "fs";
8877
+ import { createHash as createHash3 } from "crypto";
8878
+ import { readFileSync as readFileSync10 } from "fs";
8477
8879
  init_db();
8478
8880
  init_store();
8479
8881
  init_manifest();
8480
8882
  function sha256OfText(text) {
8481
- return createHash2("sha256").update(text).digest("hex");
8883
+ return createHash3("sha256").update(text).digest("hex");
8482
8884
  }
8483
8885
  function sha256OfPath(path) {
8484
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8886
+ return createHash3("sha256").update(readFileSync10(path)).digest("hex");
8485
8887
  }
8486
8888
  function collectBundledCatalog() {
8487
8889
  const out = [];
@@ -8499,7 +8901,14 @@ function collectBundledCatalog() {
8499
8901
  }
8500
8902
  async function fetchJson(base, path, apiKey, opts) {
8501
8903
  const headers = { accept: "application/json" };
8502
- if (apiKey)
8904
+ if (typeof apiKey === "object") {
8905
+ if (apiKey.origin !== base)
8906
+ throw new Error("Hooks registry authority does not match the request target");
8907
+ const resolved = await resolveHooksRequestAuthority(apiKey);
8908
+ if (resolved.origin !== base)
8909
+ throw new Error("Hooks registry authority changed before dispatch");
8910
+ headers["x-api-key"] = resolved.apiKey;
8911
+ } else if (apiKey)
8503
8912
  headers["x-api-key"] = apiKey;
8504
8913
  const res = await fetch(`${base}${path}`, {
8505
8914
  headers,
@@ -8567,8 +8976,8 @@ async function planSync(options = {}) {
8567
8976
  credentials: options.credentials
8568
8977
  });
8569
8978
  if (transport.mode === "remote" && transport.authority) {
8570
- const { origin, apiKey } = transport.authority;
8571
- const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8979
+ const { origin } = transport.authority;
8980
+ const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
8572
8981
  return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
8573
8982
  }
8574
8983
  const diff = computeDiff(collectBundledCatalog(), null);
@@ -8596,10 +9005,10 @@ async function syncHooks(options = {}) {
8596
9005
  }
8597
9006
  return plan;
8598
9007
  }
8599
- const { origin, apiKey } = transport.authority;
8600
- const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
9008
+ const { origin } = transport.authority;
9009
+ const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
8601
9010
  const diff = computeDiff(catalog, remoteLock);
8602
- const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
9011
+ const staged = await stageSyncArtifacts(origin, transport.authority, [...diff.added, ...diff.updated], remoteLock);
8603
9012
  await commitSyncArtifacts(staged, origin, remoteLock);
8604
9013
  return { apiUrl: origin, dryRun: false, diff };
8605
9014
  }
@@ -8775,9 +9184,9 @@ async function installPinnedFromBundled(name, version) {
8775
9184
  // src/serve.ts
8776
9185
  init_manifest();
8777
9186
  init_store();
8778
- import { readFileSync as readFileSync10 } from "fs";
8779
- import { dirname as dirname5, join as join11 } from "path";
8780
- import { fileURLToPath as fileURLToPath3 } from "url";
9187
+ import { readFileSync as readFileSync11 } from "fs";
9188
+ import { dirname as dirname7, join as join12 } from "path";
9189
+ import { fileURLToPath as fileURLToPath4 } from "url";
8781
9190
 
8782
9191
  // src/lib/secure-compare.ts
8783
9192
  function secureEqual(a, b) {
@@ -8785,163 +9194,364 @@ function secureEqual(a, b) {
8785
9194
  const ab = enc.encode(a);
8786
9195
  const bb = enc.encode(b);
8787
9196
  let diff = ab.length ^ bb.length;
8788
- const n = Math.max(ab.length, bb.length);
8789
- for (let i = 0;i < n; i++) {
9197
+ const n2 = Math.max(ab.length, bb.length);
9198
+ for (let i = 0;i < n2; i++) {
8790
9199
  diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
8791
9200
  }
8792
9201
  return diff === 0;
8793
9202
  }
8794
9203
 
8795
9204
  // src/openapi.ts
8796
- var openApiDocument = {
8797
- openapi: "3.1.0",
8798
- info: {
8799
- title: "Hooks Registry API",
8800
- version: "0.1.0",
8801
- description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
8802
- },
8803
- servers: [{ url: "/" }],
8804
- paths: {
8805
- "/health": {
8806
- get: {
8807
- summary: "Liveness probe",
8808
- operationId: "getHealth",
8809
- responses: {
8810
- "200": {
8811
- description: "Service is alive",
8812
- content: {
8813
- "application/json": {
8814
- schema: { type: "object", additionalProperties: true }
9205
+ function buildOpenApiDocument(version) {
9206
+ return {
9207
+ openapi: "3.1.0",
9208
+ info: {
9209
+ title: "Hooks Registry API",
9210
+ version,
9211
+ description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
9212
+ },
9213
+ servers: [{ url: "/" }],
9214
+ paths: {
9215
+ "/health": {
9216
+ get: {
9217
+ summary: "Liveness probe",
9218
+ operationId: "getHealth",
9219
+ responses: {
9220
+ "200": {
9221
+ description: "Service is alive",
9222
+ content: {
9223
+ "application/json": {
9224
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9225
+ }
8815
9226
  }
8816
9227
  }
8817
9228
  }
8818
9229
  }
8819
- }
8820
- },
8821
- "/ready": {
8822
- get: {
8823
- summary: "Readiness probe \u2014 the hooks.lock store is readable",
8824
- operationId: "getReady",
8825
- responses: {
8826
- "200": { description: "Lock store is readable" },
8827
- "503": { description: "Lock store is not readable" }
9230
+ },
9231
+ "/ready": {
9232
+ get: {
9233
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
9234
+ operationId: "getReady",
9235
+ responses: {
9236
+ "200": {
9237
+ description: "Lock store is readable",
9238
+ content: {
9239
+ "application/json": {
9240
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9241
+ }
9242
+ }
9243
+ },
9244
+ "503": {
9245
+ description: "Lock store is not readable",
9246
+ content: {
9247
+ "application/json": {
9248
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9249
+ }
9250
+ }
9251
+ }
9252
+ }
8828
9253
  }
8829
- }
8830
- },
8831
- "/version": {
8832
- get: {
8833
- summary: "Package version",
8834
- operationId: "getVersion",
8835
- responses: {
8836
- "200": {
8837
- description: "The installed @hasna/hooks version",
8838
- content: {
8839
- "application/json": {
8840
- schema: { type: "object", properties: { version: { type: "string" } } }
9254
+ },
9255
+ "/version": {
9256
+ get: {
9257
+ summary: "Package version",
9258
+ operationId: "getVersion",
9259
+ responses: {
9260
+ "200": {
9261
+ description: "The installed @hasna/hooks version",
9262
+ content: {
9263
+ "application/json": {
9264
+ schema: { $ref: "#/components/schemas/HooksVersion" }
9265
+ }
8841
9266
  }
8842
9267
  }
8843
9268
  }
8844
9269
  }
8845
- }
8846
- },
8847
- "/openapi.json": {
8848
- get: {
8849
- summary: "This OpenAPI document",
8850
- operationId: "getOpenApi",
8851
- responses: { "200": { description: "OpenAPI 3.1 document" } }
8852
- }
8853
- },
8854
- "/api/v1/catalog": {
8855
- get: {
8856
- summary: "List the full hook catalog with versions and sha256",
8857
- operationId: "getCatalog",
8858
- responses: {
8859
- "200": {
8860
- description: "Catalog entries",
8861
- content: {
8862
- "application/json": {
8863
- schema: {
8864
- type: "object",
8865
- properties: {
8866
- hooks: {
8867
- type: "array",
8868
- items: {
8869
- type: "object",
8870
- properties: {
8871
- name: { type: "string" },
8872
- version: { type: "string" },
8873
- sha256: { type: "string" },
8874
- events: { type: "array", items: { type: "string" } }
8875
- }
8876
- }
8877
- }
8878
- }
9270
+ },
9271
+ "/openapi.json": {
9272
+ get: {
9273
+ summary: "This OpenAPI document",
9274
+ operationId: "getOpenApi",
9275
+ responses: {
9276
+ "200": {
9277
+ description: "OpenAPI 3.1 document",
9278
+ content: {
9279
+ "application/json": {
9280
+ schema: { type: "object", additionalProperties: true }
8879
9281
  }
8880
9282
  }
8881
9283
  }
8882
9284
  }
8883
9285
  }
8884
- }
8885
- },
8886
- "/api/v1/lock": {
8887
- get: {
8888
- summary: "Read the published hooks.lock with per-hook versions",
8889
- operationId: "getLock",
8890
- responses: { "200": { description: "The lock file" } }
8891
- }
8892
- },
8893
- "/api/v1/hooks/{name}/{version}": {
8894
- get: {
8895
- summary: "Fetch one hook's manifest and script",
8896
- operationId: "getHookArtifact",
8897
- parameters: [
8898
- { name: "name", in: "path", required: true, schema: { type: "string" } },
8899
- { name: "version", in: "path", required: true, schema: { type: "string" } }
8900
- ],
8901
- responses: {
8902
- "200": {
8903
- description: "Hook artifact; x-hook-sha256 header carries the script hash"
9286
+ },
9287
+ "/api/v1/catalog": {
9288
+ get: {
9289
+ summary: "List the full hook catalog with versions and sha256",
9290
+ operationId: "getCatalog",
9291
+ responses: {
9292
+ "200": {
9293
+ description: "Catalog entries",
9294
+ content: {
9295
+ "application/json": {
9296
+ schema: { $ref: "#/components/schemas/HooksCatalog" }
9297
+ }
9298
+ }
9299
+ }
9300
+ }
9301
+ }
9302
+ },
9303
+ "/api/v1/lock": {
9304
+ get: {
9305
+ summary: "Read the published hooks.lock with per-hook versions",
9306
+ operationId: "getLock",
9307
+ responses: {
9308
+ "200": {
9309
+ description: "The lock file",
9310
+ content: {
9311
+ "application/json": {
9312
+ schema: { $ref: "#/components/schemas/HooksLock" }
9313
+ }
9314
+ }
9315
+ }
9316
+ }
9317
+ }
9318
+ },
9319
+ "/api/v1/hooks/{name}/{version}": {
9320
+ get: {
9321
+ summary: "Fetch one hook's manifest and script",
9322
+ operationId: "getHookArtifact",
9323
+ parameters: [
9324
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
9325
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
9326
+ ],
9327
+ responses: {
9328
+ "200": {
9329
+ description: "Hook artifact; x-hook-sha256 header carries the script hash",
9330
+ headers: {
9331
+ "x-hook-sha256": {
9332
+ description: "sha256 of the served script, the same value the lock pins",
9333
+ schema: { type: "string" }
9334
+ }
9335
+ },
9336
+ content: {
9337
+ "application/json": {
9338
+ schema: { $ref: "#/components/schemas/HooksArtifact" }
9339
+ }
9340
+ }
9341
+ },
9342
+ "400": {
9343
+ description: "Invalid URL encoding or semver version",
9344
+ content: {
9345
+ "application/json": {
9346
+ schema: { $ref: "#/components/schemas/HooksError" }
9347
+ }
9348
+ }
9349
+ },
9350
+ "404": {
9351
+ description: "Hook not found locally",
9352
+ content: {
9353
+ "application/json": {
9354
+ schema: { $ref: "#/components/schemas/HooksError" }
9355
+ }
9356
+ }
9357
+ }
9358
+ }
9359
+ }
9360
+ },
9361
+ "/api/v1/hooks": {
9362
+ put: {
9363
+ summary: "Publish/retrust a hook (requires the API key)",
9364
+ operationId: "putHook",
9365
+ requestBody: {
9366
+ required: true,
9367
+ content: {
9368
+ "application/json": {
9369
+ schema: { $ref: "#/components/schemas/HooksPublishRequest" }
9370
+ }
9371
+ }
8904
9372
  },
8905
- "400": { description: "Invalid URL encoding or semver version" },
8906
- "404": { description: "Hook not found locally" }
9373
+ responses: {
9374
+ "200": {
9375
+ description: "Hook retrusted and pin updated",
9376
+ content: {
9377
+ "application/json": {
9378
+ schema: { $ref: "#/components/schemas/HooksPublishResponse" }
9379
+ }
9380
+ }
9381
+ },
9382
+ "400": {
9383
+ description: "Invalid JSON body",
9384
+ content: {
9385
+ "application/json": {
9386
+ schema: { $ref: "#/components/schemas/HooksError" }
9387
+ }
9388
+ }
9389
+ },
9390
+ "401": {
9391
+ description: "Missing or invalid API key",
9392
+ content: {
9393
+ "application/json": {
9394
+ schema: { $ref: "#/components/schemas/HooksError" }
9395
+ }
9396
+ }
9397
+ },
9398
+ "404": {
9399
+ description: "Hook not found in local store",
9400
+ content: {
9401
+ "application/json": {
9402
+ schema: { $ref: "#/components/schemas/HooksError" }
9403
+ }
9404
+ }
9405
+ },
9406
+ "409": {
9407
+ description: "Version mismatch with the local store",
9408
+ content: {
9409
+ "application/json": {
9410
+ schema: { $ref: "#/components/schemas/HooksError" }
9411
+ }
9412
+ }
9413
+ }
9414
+ },
9415
+ security: [{ apiKey: [] }]
8907
9416
  }
8908
9417
  }
8909
9418
  },
8910
- "/api/v1/hooks": {
8911
- put: {
8912
- summary: "Publish/retrust a hook (requires the API key)",
8913
- operationId: "putHook",
8914
- requestBody: {
8915
- required: true,
8916
- content: {
8917
- "application/json": {
8918
- schema: {
8919
- type: "object",
8920
- properties: { name: { type: "string" }, version: { type: "string" } }
8921
- }
9419
+ components: {
9420
+ securitySchemes: {
9421
+ apiKey: {
9422
+ type: "http",
9423
+ scheme: "bearer",
9424
+ 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."
9425
+ }
9426
+ },
9427
+ schemas: {
9428
+ HooksHealth: {
9429
+ type: "object",
9430
+ additionalProperties: false,
9431
+ required: ["status", "name"],
9432
+ properties: {
9433
+ status: { type: "string" },
9434
+ name: { type: "string" }
9435
+ }
9436
+ },
9437
+ HooksVersion: {
9438
+ type: "object",
9439
+ additionalProperties: false,
9440
+ required: ["version"],
9441
+ properties: { version: { type: "string" } }
9442
+ },
9443
+ HooksCatalogEntry: {
9444
+ type: "object",
9445
+ additionalProperties: false,
9446
+ required: ["name", "version", "sha256", "events", "description", "source", "versions"],
9447
+ properties: {
9448
+ name: { type: "string" },
9449
+ version: { type: "string" },
9450
+ sha256: { type: "string" },
9451
+ events: { type: "array", items: { type: "string" } },
9452
+ description: { type: "string" },
9453
+ source: {
9454
+ type: "string",
9455
+ description: "bundled | custom | custom-overrides-bundled"
9456
+ },
9457
+ versions: { type: "array", items: { type: "string" } }
9458
+ }
9459
+ },
9460
+ HooksCatalog: {
9461
+ type: "object",
9462
+ additionalProperties: false,
9463
+ required: ["hooks"],
9464
+ properties: {
9465
+ hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
9466
+ }
9467
+ },
9468
+ HooksLockEntry: {
9469
+ type: "object",
9470
+ additionalProperties: false,
9471
+ required: ["version", "sha256", "source"],
9472
+ properties: {
9473
+ version: { type: "string" },
9474
+ sha256: { type: "string" },
9475
+ source: { type: "string" },
9476
+ pinned: {
9477
+ type: "boolean",
9478
+ description: "True for an explicit install/update pin, absent for sync-maintained pins."
9479
+ },
9480
+ versions: { type: "array", items: { type: "string" } }
9481
+ }
9482
+ },
9483
+ HooksLock: {
9484
+ type: "object",
9485
+ additionalProperties: false,
9486
+ required: ["hooks"],
9487
+ properties: {
9488
+ hooks: {
9489
+ type: "object",
9490
+ additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
8922
9491
  }
8923
9492
  }
8924
9493
  },
8925
- responses: {
8926
- "200": { description: "Hook retrusted and pin updated" },
8927
- "401": { description: "Missing or invalid API key" },
8928
- "404": { description: "Hook not found in local store" },
8929
- "409": { description: "Version mismatch with the local store" }
9494
+ HooksManifest: {
9495
+ type: "object",
9496
+ additionalProperties: false,
9497
+ required: ["name", "version", "events", "script"],
9498
+ properties: {
9499
+ name: { type: "string" },
9500
+ version: { type: "string" },
9501
+ description: { type: "string" },
9502
+ events: { type: "array", items: { type: "string" } },
9503
+ script: { type: "string" },
9504
+ script_kind: { type: "string", enum: ["inline", "file"] },
9505
+ args: { type: "array", items: { type: "string" } },
9506
+ timeout_ms: { type: "number" }
9507
+ }
8930
9508
  },
8931
- security: [{ apiKey: [] }]
8932
- }
8933
- }
8934
- },
8935
- components: {
8936
- securitySchemes: {
8937
- apiKey: {
8938
- type: "http",
8939
- scheme: "bearer",
8940
- 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."
9509
+ HooksArtifact: {
9510
+ type: "object",
9511
+ additionalProperties: false,
9512
+ required: ["manifest", "script"],
9513
+ properties: {
9514
+ manifest: { $ref: "#/components/schemas/HooksManifest" },
9515
+ script: { type: "string" }
9516
+ }
9517
+ },
9518
+ HooksError: {
9519
+ type: "object",
9520
+ additionalProperties: false,
9521
+ required: ["error"],
9522
+ properties: { error: { type: "string" } }
9523
+ },
9524
+ HooksPublishRequest: {
9525
+ type: "object",
9526
+ additionalProperties: false,
9527
+ required: ["name"],
9528
+ properties: {
9529
+ name: { type: "string" },
9530
+ version: { type: "string" }
9531
+ }
9532
+ },
9533
+ HooksPublishResponse: {
9534
+ type: "object",
9535
+ additionalProperties: false,
9536
+ required: ["ok", "hook"],
9537
+ properties: {
9538
+ ok: { type: "boolean" },
9539
+ hook: {
9540
+ type: "object",
9541
+ additionalProperties: false,
9542
+ required: ["name", "version", "sha256"],
9543
+ properties: {
9544
+ name: { type: "string" },
9545
+ version: { type: "string" },
9546
+ sha256: { type: "string" }
9547
+ }
9548
+ }
9549
+ }
9550
+ }
8941
9551
  }
8942
9552
  }
8943
- }
8944
- };
9553
+ };
9554
+ }
8945
9555
 
8946
9556
  // src/serve.ts
8947
9557
  init_semver();
@@ -8950,8 +9560,8 @@ var SERVE_HOST = "127.0.0.1";
8950
9560
  var SERVE_SERVICE_NAME = "hooks-registry";
8951
9561
  function packageVersion() {
8952
9562
  try {
8953
- const pkgPath = join11(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
8954
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
9563
+ const pkgPath = join12(dirname7(fileURLToPath4(import.meta.url)), "..", "package.json");
9564
+ return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
8955
9565
  } catch {
8956
9566
  return "0.0.0";
8957
9567
  }
@@ -9040,7 +9650,7 @@ function handleServeRequest(req, apiKey) {
9040
9650
  return Promise.resolve(json({ version: packageVersion() }));
9041
9651
  }
9042
9652
  if (url.pathname === "/openapi.json" && req.method === "GET") {
9043
- return Promise.resolve(json(openApiDocument));
9653
+ return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
9044
9654
  }
9045
9655
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
9046
9656
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
@@ -9112,7 +9722,7 @@ function startServeServer(options) {
9112
9722
  hostname: host,
9113
9723
  port,
9114
9724
  async fetch(req) {
9115
- return handleServeRequest(req, resolveHooksServePublishKey());
9725
+ return handleServeRequest(req, await resolveHooksServePublishKeyAsync());
9116
9726
  }
9117
9727
  });
9118
9728
  console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
@@ -9217,11 +9827,11 @@ async function runHook(name, input, options = {}) {
9217
9827
  throw new Error(`Hook '${name}' not found`);
9218
9828
  const hookScript = getHookPath(name) + "/src/hook.ts";
9219
9829
  const resolvedScript = custom2?.scriptPath;
9220
- const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
9830
+ const script = resolvedScript ?? (existsSync13(hookScript) ? hookScript : undefined);
9221
9831
  if (!script)
9222
9832
  throw new Error(`Hook script not found: ${name}`);
9223
9833
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
9224
- const content = readFileSync11(script);
9834
+ const content = readFileSync12(script);
9225
9835
  const check = checkScriptHash2(name, sha256Of2(content));
9226
9836
  if (!check.ok) {
9227
9837
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
@@ -9334,7 +9944,9 @@ export {
9334
9944
  resolveScriptPath,
9335
9945
  resolveScript,
9336
9946
  resolveHooksTransport,
9947
+ resolveHooksServePublishKeyAsync,
9337
9948
  resolveHooksServePublishKey,
9949
+ resolveHooksRequestAuthority,
9338
9950
  resolveHookMeta,
9339
9951
  resolveHookEventSink,
9340
9952
  resolveHookDir,