@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/bin/serve.js CHANGED
@@ -174,6 +174,7 @@ var HOOKS = [
174
174
  },
175
175
  {
176
176
  name: "workspace-repos-guard",
177
+ timeoutSeconds: 10,
177
178
  displayName: "Workspace Repos Guard",
178
179
  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",
179
180
  version: "0.1.0",
@@ -192,7 +193,7 @@ var HOOKS = [
192
193
  matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
193
194
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
194
195
  rewritesInput: true,
195
- timeoutSeconds: 5
196
+ timeoutSeconds: 10
196
197
  },
197
198
  {
198
199
  name: "checktests",
@@ -5358,16 +5359,21 @@ function resolveHook(name) {
5358
5359
 
5359
5360
  // src/serve.ts
5360
5361
  import { readFileSync as readFileSync5 } from "fs";
5361
- import { dirname as dirname3, join as join8 } from "path";
5362
- import { fileURLToPath as fileURLToPath2 } from "url";
5362
+ import { dirname as dirname4, join as join8 } from "path";
5363
+ import { fileURLToPath as fileURLToPath5 } from "url";
5363
5364
 
5364
- // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
5365
+ // ../contracts/dist/client/transport.js
5365
5366
  import { spawnSync } from "child_process";
5366
- import { closeSync, fstatSync, openSync, readFileSync as readFileSync4 } from "fs";
5367
- import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5368
- import { createRequire } from "module";
5367
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
5368
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
5369
5369
  import { hostname as osHostname } from "os";
5370
- import { isAbsolute as isAbsolute2, join as join7 } from "path";
5370
+ import { isAbsolute as isAbsolute2, join as join22 } from "path";
5371
+ import { createRequire } from "module";
5372
+ import { fileURLToPath as fileURLToPath2 } from "url";
5373
+ import { closeSync, fstatSync, lstatSync, openSync, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync } from "fs";
5374
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5375
+ import { basename as basename2, dirname as dirname3, join as join7, relative, sep as sep2 } from "path";
5376
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
5371
5377
  function envToken(name) {
5372
5378
  return name.toUpperCase().replace(/-/g, "_");
5373
5379
  }
@@ -5385,6 +5391,114 @@ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
5385
5391
  function credentialPointerEnvKey(name) {
5386
5392
  return `HASNA_${envToken(name)}_API_KEY_REF`;
5387
5393
  }
5394
+ function e(e2, n, r) {
5395
+ throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
5396
+ }
5397
+ function n(n2, i, o, f) {
5398
+ let s, u, l = r(n2, o), c = function(e2) {
5399
+ let n3 = new Set(["default", ...e2.conditions || []]);
5400
+ return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
5401
+ }(f || {}), a = i[l];
5402
+ if (a === undefined) {
5403
+ let e2, n3, r, t;
5404
+ for (t in i)
5405
+ 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))));
5406
+ a = i[n3];
5407
+ }
5408
+ return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
5409
+ let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
5410
+ for (;t < i2; t++)
5411
+ e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
5412
+ }(s, u), s;
5413
+ }
5414
+ function r(e2, n2, r2) {
5415
+ if (e2 === n2 || n2 === ".")
5416
+ return ".";
5417
+ let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
5418
+ return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
5419
+ }
5420
+ function t(e2, n2, r2) {
5421
+ if (e2) {
5422
+ if (typeof e2 == "string")
5423
+ return r2 && r2.add(e2), [e2];
5424
+ let i, o;
5425
+ if (Array.isArray(e2)) {
5426
+ for (o = r2 || new Set, i = 0;i < e2.length; i++)
5427
+ t(e2[i], n2, o);
5428
+ if (!r2 && o.size)
5429
+ return [...o];
5430
+ } else
5431
+ for (i in e2)
5432
+ if (n2.has(i))
5433
+ return t(e2[i], n2, r2);
5434
+ }
5435
+ }
5436
+ function o(e2, r2, t2) {
5437
+ let i, o2 = e2.exports;
5438
+ if (o2) {
5439
+ if (typeof o2 == "string")
5440
+ o2 = { ".": o2 };
5441
+ else
5442
+ for (i in o2) {
5443
+ i[0] !== "." && (o2 = { ".": o2 });
5444
+ break;
5445
+ }
5446
+ return n(e2.name, o2, r2 || ".", t2);
5447
+ }
5448
+ }
5449
+ var PACKAGE = "@hasna/secrets";
5450
+ var MAX_PACKAGE_BYTES = 1024 * 1024;
5451
+ function entryPoint(directory) {
5452
+ const root = realpathSync2(directory);
5453
+ const fd = openSync(join7(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5454
+ let pkg;
5455
+ try {
5456
+ const stat = fstatSync(fd);
5457
+ if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
5458
+ throw new Error("Invalid Secrets package metadata");
5459
+ pkg = JSON.parse(readFileSync4(fd, "utf8"));
5460
+ } finally {
5461
+ closeSync(fd);
5462
+ }
5463
+ if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
5464
+ throw new Error("Invalid Secrets package identity");
5465
+ }
5466
+ const target = o(pkg, ".")?.[0];
5467
+ if (typeof target !== "string" || !target.startsWith("./"))
5468
+ throw new Error("Missing Secrets import export");
5469
+ const parts = target.slice(2).split("/");
5470
+ if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
5471
+ throw new Error("Invalid Secrets import export");
5472
+ }
5473
+ const file = realpathSync2(join7(root, ...parts));
5474
+ const within = relative(root, file);
5475
+ if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
5476
+ throw new Error("Secrets import export escapes its package or is not a file");
5477
+ }
5478
+ return pathToFileURL(file).href;
5479
+ }
5480
+ function resolveInstalledSecrets(parentUrl) {
5481
+ let directory = dirname3(fileURLToPath3(parentUrl));
5482
+ for (;; ) {
5483
+ if (basename2(directory) !== "node_modules") {
5484
+ const candidate = join7(directory, "node_modules", PACKAGE);
5485
+ let absent = false;
5486
+ try {
5487
+ lstatSync(candidate);
5488
+ } catch (error) {
5489
+ if (error.code !== "ENOENT")
5490
+ throw error;
5491
+ absent = true;
5492
+ }
5493
+ if (!absent)
5494
+ return entryPoint(candidate);
5495
+ }
5496
+ const parent = dirname3(directory);
5497
+ if (parent === directory)
5498
+ throw new Error("Secrets SDK is not installed for this consumer");
5499
+ directory = parent;
5500
+ }
5501
+ }
5388
5502
 
5389
5503
  class CredentialResolutionError extends Error {
5390
5504
  appName;
@@ -5433,14 +5547,14 @@ function hasnaHomeDir(env) {
5433
5547
  if (override)
5434
5548
  return override;
5435
5549
  const home = homeDir(env);
5436
- return home ? join7(home, HASNA_HOME_DIR) : null;
5550
+ return home ? join22(home, HASNA_HOME_DIR) : null;
5437
5551
  }
5438
5552
  function appConfigDir(name, env) {
5439
5553
  const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
5440
5554
  if (configRoot)
5441
- return join7(configRoot, name);
5555
+ return join22(configRoot, name);
5442
5556
  const root = hasnaHomeDir(env);
5443
- return root ? join7(root, name, CONFIG_SUBDIR) : null;
5557
+ return root ? join22(root, name, CONFIG_SUBDIR) : null;
5444
5558
  }
5445
5559
  function credentialDiskSourceList(name, env, profile = null) {
5446
5560
  if (!SAFE_APP_SLUG.test(name))
@@ -5449,7 +5563,7 @@ function credentialDiskSourceList(name, env, profile = null) {
5449
5563
  if (!directory)
5450
5564
  return [];
5451
5565
  const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
5452
- return [{ path: join7(directory, file), tier: "disk" }];
5566
+ return [{ path: join22(directory, file), tier: "disk" }];
5453
5567
  }
5454
5568
  function credentialDiskSources(name, env) {
5455
5569
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -5503,7 +5617,7 @@ function readAppConfigFile(path) {
5503
5617
  };
5504
5618
  let fd = -1;
5505
5619
  try {
5506
- fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5620
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
5507
5621
  } catch (error) {
5508
5622
  const code = error.code;
5509
5623
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -5513,7 +5627,7 @@ function readAppConfigFile(path) {
5513
5627
  unsafe(`the path could not be opened (${code ?? "unknown error"})`);
5514
5628
  }
5515
5629
  try {
5516
- const before = fstatSync(fd);
5630
+ const before = fstatSync2(fd);
5517
5631
  if (!before.isFile())
5518
5632
  unsafe("the path is not a regular file");
5519
5633
  if (!configFileModeAllowed(before.mode)) {
@@ -5524,31 +5638,39 @@ function readAppConfigFile(path) {
5524
5638
  unsafe("the file is not owned by the current user");
5525
5639
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
5526
5640
  unsafe("the file exceeds the size limit");
5527
- const bytes = readFileSync4(fd);
5528
- const after = fstatSync(fd);
5641
+ const bytes = readFileSync22(fd);
5642
+ const after = fstatSync2(fd);
5529
5643
  if (!configFileReadsCoherent(before, after)) {
5530
5644
  unsafe("the file changed while being read");
5531
5645
  }
5532
5646
  return parseEnvFile(bytes.toString("utf8"));
5533
5647
  } finally {
5534
5648
  if (fd !== -1)
5535
- closeSync(fd);
5649
+ closeSync2(fd);
5536
5650
  }
5537
5651
  }
5538
- function readCredentialFile(path, apiKeyKeys) {
5652
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
5539
5653
  const parsed = readAppConfigFile(path);
5540
5654
  if (!parsed)
5541
5655
  return null;
5542
- for (const key of apiKeyKeys) {
5656
+ for (const key of [...apiKeyKeys, pointerKey]) {
5543
5657
  if (parsed.unusable.has(key)) {
5544
5658
  throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
5545
5659
  }
5546
5660
  }
5547
5661
  const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
5662
+ const pointer = parsed.values.get(pointerKey)?.trim();
5663
+ if (pointer !== undefined) {
5664
+ if (!VAULT_POINTER_SHAPE.test(pointer))
5665
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
5666
+ if (values.length)
5667
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
5668
+ return { apiKey: "", pointerVaultKey: pointer };
5669
+ }
5548
5670
  if (new Set(values).size > 1) {
5549
5671
  throw new CredentialFileUnsafeError(path, "credential aliases disagree");
5550
5672
  }
5551
- return values[0] ?? null;
5673
+ return values[0] === undefined ? null : { apiKey: values[0] };
5552
5674
  }
5553
5675
  function assertUsableCredential(appName, source, value) {
5554
5676
  if (VAULT_POINTER_SHAPE.test(value)) {
@@ -5781,12 +5903,13 @@ function resolveCredential(name, env, options = {}) {
5781
5903
  }
5782
5904
  const paths = profileDiskSources(name, env, profile);
5783
5905
  for (const path of paths) {
5784
- const value = readCredentialFile(path, apiKeyKeys);
5906
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
5785
5907
  if (value) {
5786
- assertUsableCredential(name, path, value);
5908
+ if (!value.pointerVaultKey)
5909
+ assertUsableCredential(name, path, value.apiKey);
5787
5910
  return sealCredential({
5788
- apiKey: value,
5789
- tier: "profile",
5911
+ ...value,
5912
+ tier: value.pointerVaultKey ? "pointer" : "profile",
5790
5913
  source: path,
5791
5914
  deliberate: true,
5792
5915
  diskCandidates: paths,
@@ -5794,7 +5917,7 @@ function resolveCredential(name, env, options = {}) {
5794
5917
  });
5795
5918
  }
5796
5919
  }
5797
- 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);
5920
+ 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);
5798
5921
  }
5799
5922
  const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
5800
5923
  const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
@@ -5819,17 +5942,27 @@ function resolveCredential(name, env, options = {}) {
5819
5942
  });
5820
5943
  }
5821
5944
  const diskSourceList = credentialDiskSourceList(name, env, null);
5822
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
5945
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
5823
5946
  if (diskHits.length > 0) {
5824
5947
  const winner = diskHits[0];
5825
- assertUsableCredential(name, winner.src.path, winner.value);
5948
+ if (winner.value.pointerVaultKey) {
5949
+ return sealCredential({
5950
+ ...winner.value,
5951
+ tier: "pointer",
5952
+ source: winner.src.path,
5953
+ deliberate: false,
5954
+ diskCandidates: diskPaths,
5955
+ warning: null
5956
+ });
5957
+ }
5958
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
5826
5959
  const divergentSources = [
5827
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
5828
- ...envHit && envHit.value !== winner.value ? [envHit.key] : []
5960
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
5961
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
5829
5962
  ];
5830
5963
  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;
5831
5964
  return sealCredential({
5832
- apiKey: winner.value,
5965
+ apiKey: winner.value.apiKey,
5833
5966
  tier: winner.src.tier,
5834
5967
  source: winner.src.path,
5835
5968
  deliberate: false,
@@ -5850,8 +5983,54 @@ function resolveCredential(name, env, options = {}) {
5850
5983
  }
5851
5984
  return null;
5852
5985
  }
5853
- var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
5854
5986
  var requireSecretsSdk = createRequire(import.meta.url);
5987
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
5988
+ const secretsEnv = snapshotClientEnvironment("secrets", env);
5989
+ const vaultKey = pointerResolution.pointerVaultKey;
5990
+ const pointerEnvKey = pointerResolution.source;
5991
+ if (!vaultKey) {
5992
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
5993
+ }
5994
+ if (name === "secrets") {
5995
+ throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
5996
+ }
5997
+ let secretsSdk;
5998
+ try {
5999
+ const sdkUrl = resolveInstalledSecrets(import.meta.url);
6000
+ secretsSdk = requireSecretsSdk(fileURLToPath2(sdkUrl));
6001
+ } catch {
6002
+ 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]);
6003
+ }
6004
+ let client;
6005
+ try {
6006
+ const bootstrap = resolveCredential("secrets", secretsEnv);
6007
+ if (!bootstrap || bootstrap.tier === "pointer") {
6008
+ throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
6009
+ }
6010
+ client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
6011
+ } catch {
6012
+ 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]);
6013
+ }
6014
+ let secret;
6015
+ try {
6016
+ secret = await client.getSecret({ key: vaultKey });
6017
+ } catch {
6018
+ 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]);
6019
+ }
6020
+ const value = secret.value;
6021
+ if (!value) {
6022
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
6023
+ }
6024
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
6025
+ return sealCredential({
6026
+ apiKey: value,
6027
+ tier: "pointer",
6028
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
6029
+ deliberate: pointerResolution.deliberate,
6030
+ diskCandidates: pointerResolution.diskCandidates,
6031
+ warning: null
6032
+ });
6033
+ }
5855
6034
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
5856
6035
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
5857
6036
  "host",
@@ -5899,11 +6078,25 @@ function hooksResolverInputs(env, credentials = {}) {
5899
6078
  }
5900
6079
 
5901
6080
  // src/lib/transport.ts
5902
- function resolveHooksServePublishKey(env = process.env, options = {}) {
6081
+ var authorityBindings = new WeakMap;
6082
+ function sameCredential(left, right) {
6083
+ return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
6084
+ }
6085
+ function changedBinding() {
6086
+ throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
6087
+ }
6088
+ async function resolveHooksServePublishKeyAsync(env = process.env, options = {}) {
5903
6089
  const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
5904
- const tier1 = options.credentials?.apiKey;
5905
- const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
5906
- return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
6090
+ const read = () => resolveCredential("hooks", resolverEnv, credentials);
6091
+ const selected = read();
6092
+ if (!selected || selected.tier !== "pointer")
6093
+ return selected?.apiKey;
6094
+ if (!sameCredential(selected, read()))
6095
+ changedBinding();
6096
+ const completed = await completePointerCredential("hooks", selected, env);
6097
+ if (!sameCredential(selected, read()))
6098
+ changedBinding();
6099
+ return completed.apiKey;
5907
6100
  }
5908
6101
 
5909
6102
  // src/lib/secure-compare.ts
@@ -5912,163 +6105,364 @@ function secureEqual(a, b) {
5912
6105
  const ab = enc.encode(a);
5913
6106
  const bb = enc.encode(b);
5914
6107
  let diff = ab.length ^ bb.length;
5915
- const n = Math.max(ab.length, bb.length);
5916
- for (let i = 0;i < n; i++) {
6108
+ const n2 = Math.max(ab.length, bb.length);
6109
+ for (let i = 0;i < n2; i++) {
5917
6110
  diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
5918
6111
  }
5919
6112
  return diff === 0;
5920
6113
  }
5921
6114
 
5922
6115
  // src/openapi.ts
5923
- var openApiDocument = {
5924
- openapi: "3.1.0",
5925
- info: {
5926
- title: "Hooks Registry API",
5927
- version: "0.1.0",
5928
- 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)."
5929
- },
5930
- servers: [{ url: "/" }],
5931
- paths: {
5932
- "/health": {
5933
- get: {
5934
- summary: "Liveness probe",
5935
- operationId: "getHealth",
5936
- responses: {
5937
- "200": {
5938
- description: "Service is alive",
5939
- content: {
5940
- "application/json": {
5941
- schema: { type: "object", additionalProperties: true }
6116
+ function buildOpenApiDocument(version) {
6117
+ return {
6118
+ openapi: "3.1.0",
6119
+ info: {
6120
+ title: "Hooks Registry API",
6121
+ version,
6122
+ 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)."
6123
+ },
6124
+ servers: [{ url: "/" }],
6125
+ paths: {
6126
+ "/health": {
6127
+ get: {
6128
+ summary: "Liveness probe",
6129
+ operationId: "getHealth",
6130
+ responses: {
6131
+ "200": {
6132
+ description: "Service is alive",
6133
+ content: {
6134
+ "application/json": {
6135
+ schema: { $ref: "#/components/schemas/HooksHealth" }
6136
+ }
5942
6137
  }
5943
6138
  }
5944
6139
  }
5945
6140
  }
5946
- }
5947
- },
5948
- "/ready": {
5949
- get: {
5950
- summary: "Readiness probe \u2014 the hooks.lock store is readable",
5951
- operationId: "getReady",
5952
- responses: {
5953
- "200": { description: "Lock store is readable" },
5954
- "503": { description: "Lock store is not readable" }
6141
+ },
6142
+ "/ready": {
6143
+ get: {
6144
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
6145
+ operationId: "getReady",
6146
+ responses: {
6147
+ "200": {
6148
+ description: "Lock store is readable",
6149
+ content: {
6150
+ "application/json": {
6151
+ schema: { $ref: "#/components/schemas/HooksHealth" }
6152
+ }
6153
+ }
6154
+ },
6155
+ "503": {
6156
+ description: "Lock store is not readable",
6157
+ content: {
6158
+ "application/json": {
6159
+ schema: { $ref: "#/components/schemas/HooksHealth" }
6160
+ }
6161
+ }
6162
+ }
6163
+ }
5955
6164
  }
5956
- }
5957
- },
5958
- "/version": {
5959
- get: {
5960
- summary: "Package version",
5961
- operationId: "getVersion",
5962
- responses: {
5963
- "200": {
5964
- description: "The installed @hasna/hooks version",
5965
- content: {
5966
- "application/json": {
5967
- schema: { type: "object", properties: { version: { type: "string" } } }
6165
+ },
6166
+ "/version": {
6167
+ get: {
6168
+ summary: "Package version",
6169
+ operationId: "getVersion",
6170
+ responses: {
6171
+ "200": {
6172
+ description: "The installed @hasna/hooks version",
6173
+ content: {
6174
+ "application/json": {
6175
+ schema: { $ref: "#/components/schemas/HooksVersion" }
6176
+ }
5968
6177
  }
5969
6178
  }
5970
6179
  }
5971
6180
  }
5972
- }
5973
- },
5974
- "/openapi.json": {
5975
- get: {
5976
- summary: "This OpenAPI document",
5977
- operationId: "getOpenApi",
5978
- responses: { "200": { description: "OpenAPI 3.1 document" } }
5979
- }
5980
- },
5981
- "/api/v1/catalog": {
5982
- get: {
5983
- summary: "List the full hook catalog with versions and sha256",
5984
- operationId: "getCatalog",
5985
- responses: {
5986
- "200": {
5987
- description: "Catalog entries",
5988
- content: {
5989
- "application/json": {
5990
- schema: {
5991
- type: "object",
5992
- properties: {
5993
- hooks: {
5994
- type: "array",
5995
- items: {
5996
- type: "object",
5997
- properties: {
5998
- name: { type: "string" },
5999
- version: { type: "string" },
6000
- sha256: { type: "string" },
6001
- events: { type: "array", items: { type: "string" } }
6002
- }
6003
- }
6004
- }
6005
- }
6181
+ },
6182
+ "/openapi.json": {
6183
+ get: {
6184
+ summary: "This OpenAPI document",
6185
+ operationId: "getOpenApi",
6186
+ responses: {
6187
+ "200": {
6188
+ description: "OpenAPI 3.1 document",
6189
+ content: {
6190
+ "application/json": {
6191
+ schema: { type: "object", additionalProperties: true }
6006
6192
  }
6007
6193
  }
6008
6194
  }
6009
6195
  }
6010
6196
  }
6011
- }
6012
- },
6013
- "/api/v1/lock": {
6014
- get: {
6015
- summary: "Read the published hooks.lock with per-hook versions",
6016
- operationId: "getLock",
6017
- responses: { "200": { description: "The lock file" } }
6018
- }
6019
- },
6020
- "/api/v1/hooks/{name}/{version}": {
6021
- get: {
6022
- summary: "Fetch one hook's manifest and script",
6023
- operationId: "getHookArtifact",
6024
- parameters: [
6025
- { name: "name", in: "path", required: true, schema: { type: "string" } },
6026
- { name: "version", in: "path", required: true, schema: { type: "string" } }
6027
- ],
6028
- responses: {
6029
- "200": {
6030
- description: "Hook artifact; x-hook-sha256 header carries the script hash"
6197
+ },
6198
+ "/api/v1/catalog": {
6199
+ get: {
6200
+ summary: "List the full hook catalog with versions and sha256",
6201
+ operationId: "getCatalog",
6202
+ responses: {
6203
+ "200": {
6204
+ description: "Catalog entries",
6205
+ content: {
6206
+ "application/json": {
6207
+ schema: { $ref: "#/components/schemas/HooksCatalog" }
6208
+ }
6209
+ }
6210
+ }
6211
+ }
6212
+ }
6213
+ },
6214
+ "/api/v1/lock": {
6215
+ get: {
6216
+ summary: "Read the published hooks.lock with per-hook versions",
6217
+ operationId: "getLock",
6218
+ responses: {
6219
+ "200": {
6220
+ description: "The lock file",
6221
+ content: {
6222
+ "application/json": {
6223
+ schema: { $ref: "#/components/schemas/HooksLock" }
6224
+ }
6225
+ }
6226
+ }
6227
+ }
6228
+ }
6229
+ },
6230
+ "/api/v1/hooks/{name}/{version}": {
6231
+ get: {
6232
+ summary: "Fetch one hook's manifest and script",
6233
+ operationId: "getHookArtifact",
6234
+ parameters: [
6235
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
6236
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
6237
+ ],
6238
+ responses: {
6239
+ "200": {
6240
+ description: "Hook artifact; x-hook-sha256 header carries the script hash",
6241
+ headers: {
6242
+ "x-hook-sha256": {
6243
+ description: "sha256 of the served script, the same value the lock pins",
6244
+ schema: { type: "string" }
6245
+ }
6246
+ },
6247
+ content: {
6248
+ "application/json": {
6249
+ schema: { $ref: "#/components/schemas/HooksArtifact" }
6250
+ }
6251
+ }
6252
+ },
6253
+ "400": {
6254
+ description: "Invalid URL encoding or semver version",
6255
+ content: {
6256
+ "application/json": {
6257
+ schema: { $ref: "#/components/schemas/HooksError" }
6258
+ }
6259
+ }
6260
+ },
6261
+ "404": {
6262
+ description: "Hook not found locally",
6263
+ content: {
6264
+ "application/json": {
6265
+ schema: { $ref: "#/components/schemas/HooksError" }
6266
+ }
6267
+ }
6268
+ }
6269
+ }
6270
+ }
6271
+ },
6272
+ "/api/v1/hooks": {
6273
+ put: {
6274
+ summary: "Publish/retrust a hook (requires the API key)",
6275
+ operationId: "putHook",
6276
+ requestBody: {
6277
+ required: true,
6278
+ content: {
6279
+ "application/json": {
6280
+ schema: { $ref: "#/components/schemas/HooksPublishRequest" }
6281
+ }
6282
+ }
6031
6283
  },
6032
- "400": { description: "Invalid URL encoding or semver version" },
6033
- "404": { description: "Hook not found locally" }
6284
+ responses: {
6285
+ "200": {
6286
+ description: "Hook retrusted and pin updated",
6287
+ content: {
6288
+ "application/json": {
6289
+ schema: { $ref: "#/components/schemas/HooksPublishResponse" }
6290
+ }
6291
+ }
6292
+ },
6293
+ "400": {
6294
+ description: "Invalid JSON body",
6295
+ content: {
6296
+ "application/json": {
6297
+ schema: { $ref: "#/components/schemas/HooksError" }
6298
+ }
6299
+ }
6300
+ },
6301
+ "401": {
6302
+ description: "Missing or invalid API key",
6303
+ content: {
6304
+ "application/json": {
6305
+ schema: { $ref: "#/components/schemas/HooksError" }
6306
+ }
6307
+ }
6308
+ },
6309
+ "404": {
6310
+ description: "Hook not found in local store",
6311
+ content: {
6312
+ "application/json": {
6313
+ schema: { $ref: "#/components/schemas/HooksError" }
6314
+ }
6315
+ }
6316
+ },
6317
+ "409": {
6318
+ description: "Version mismatch with the local store",
6319
+ content: {
6320
+ "application/json": {
6321
+ schema: { $ref: "#/components/schemas/HooksError" }
6322
+ }
6323
+ }
6324
+ }
6325
+ },
6326
+ security: [{ apiKey: [] }]
6034
6327
  }
6035
6328
  }
6036
6329
  },
6037
- "/api/v1/hooks": {
6038
- put: {
6039
- summary: "Publish/retrust a hook (requires the API key)",
6040
- operationId: "putHook",
6041
- requestBody: {
6042
- required: true,
6043
- content: {
6044
- "application/json": {
6045
- schema: {
6046
- type: "object",
6047
- properties: { name: { type: "string" }, version: { type: "string" } }
6048
- }
6330
+ components: {
6331
+ securitySchemes: {
6332
+ apiKey: {
6333
+ type: "http",
6334
+ scheme: "bearer",
6335
+ 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."
6336
+ }
6337
+ },
6338
+ schemas: {
6339
+ HooksHealth: {
6340
+ type: "object",
6341
+ additionalProperties: false,
6342
+ required: ["status", "name"],
6343
+ properties: {
6344
+ status: { type: "string" },
6345
+ name: { type: "string" }
6346
+ }
6347
+ },
6348
+ HooksVersion: {
6349
+ type: "object",
6350
+ additionalProperties: false,
6351
+ required: ["version"],
6352
+ properties: { version: { type: "string" } }
6353
+ },
6354
+ HooksCatalogEntry: {
6355
+ type: "object",
6356
+ additionalProperties: false,
6357
+ required: ["name", "version", "sha256", "events", "description", "source", "versions"],
6358
+ properties: {
6359
+ name: { type: "string" },
6360
+ version: { type: "string" },
6361
+ sha256: { type: "string" },
6362
+ events: { type: "array", items: { type: "string" } },
6363
+ description: { type: "string" },
6364
+ source: {
6365
+ type: "string",
6366
+ description: "bundled | custom | custom-overrides-bundled"
6367
+ },
6368
+ versions: { type: "array", items: { type: "string" } }
6369
+ }
6370
+ },
6371
+ HooksCatalog: {
6372
+ type: "object",
6373
+ additionalProperties: false,
6374
+ required: ["hooks"],
6375
+ properties: {
6376
+ hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
6377
+ }
6378
+ },
6379
+ HooksLockEntry: {
6380
+ type: "object",
6381
+ additionalProperties: false,
6382
+ required: ["version", "sha256", "source"],
6383
+ properties: {
6384
+ version: { type: "string" },
6385
+ sha256: { type: "string" },
6386
+ source: { type: "string" },
6387
+ pinned: {
6388
+ type: "boolean",
6389
+ description: "True for an explicit install/update pin, absent for sync-maintained pins."
6390
+ },
6391
+ versions: { type: "array", items: { type: "string" } }
6392
+ }
6393
+ },
6394
+ HooksLock: {
6395
+ type: "object",
6396
+ additionalProperties: false,
6397
+ required: ["hooks"],
6398
+ properties: {
6399
+ hooks: {
6400
+ type: "object",
6401
+ additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
6049
6402
  }
6050
6403
  }
6051
6404
  },
6052
- responses: {
6053
- "200": { description: "Hook retrusted and pin updated" },
6054
- "401": { description: "Missing or invalid API key" },
6055
- "404": { description: "Hook not found in local store" },
6056
- "409": { description: "Version mismatch with the local store" }
6405
+ HooksManifest: {
6406
+ type: "object",
6407
+ additionalProperties: false,
6408
+ required: ["name", "version", "events", "script"],
6409
+ properties: {
6410
+ name: { type: "string" },
6411
+ version: { type: "string" },
6412
+ description: { type: "string" },
6413
+ events: { type: "array", items: { type: "string" } },
6414
+ script: { type: "string" },
6415
+ script_kind: { type: "string", enum: ["inline", "file"] },
6416
+ args: { type: "array", items: { type: "string" } },
6417
+ timeout_ms: { type: "number" }
6418
+ }
6057
6419
  },
6058
- security: [{ apiKey: [] }]
6059
- }
6060
- }
6061
- },
6062
- components: {
6063
- securitySchemes: {
6064
- apiKey: {
6065
- type: "http",
6066
- scheme: "bearer",
6067
- 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."
6420
+ HooksArtifact: {
6421
+ type: "object",
6422
+ additionalProperties: false,
6423
+ required: ["manifest", "script"],
6424
+ properties: {
6425
+ manifest: { $ref: "#/components/schemas/HooksManifest" },
6426
+ script: { type: "string" }
6427
+ }
6428
+ },
6429
+ HooksError: {
6430
+ type: "object",
6431
+ additionalProperties: false,
6432
+ required: ["error"],
6433
+ properties: { error: { type: "string" } }
6434
+ },
6435
+ HooksPublishRequest: {
6436
+ type: "object",
6437
+ additionalProperties: false,
6438
+ required: ["name"],
6439
+ properties: {
6440
+ name: { type: "string" },
6441
+ version: { type: "string" }
6442
+ }
6443
+ },
6444
+ HooksPublishResponse: {
6445
+ type: "object",
6446
+ additionalProperties: false,
6447
+ required: ["ok", "hook"],
6448
+ properties: {
6449
+ ok: { type: "boolean" },
6450
+ hook: {
6451
+ type: "object",
6452
+ additionalProperties: false,
6453
+ required: ["name", "version", "sha256"],
6454
+ properties: {
6455
+ name: { type: "string" },
6456
+ version: { type: "string" },
6457
+ sha256: { type: "string" }
6458
+ }
6459
+ }
6460
+ }
6461
+ }
6068
6462
  }
6069
6463
  }
6070
- }
6071
- };
6464
+ };
6465
+ }
6072
6466
 
6073
6467
  // src/serve.ts
6074
6468
  var DEFAULT_SERVE_PORT = 39428;
@@ -6076,7 +6470,7 @@ var SERVE_HOST = "127.0.0.1";
6076
6470
  var SERVE_SERVICE_NAME = "hooks-registry";
6077
6471
  function packageVersion() {
6078
6472
  try {
6079
- const pkgPath = join8(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
6473
+ const pkgPath = join8(dirname4(fileURLToPath5(import.meta.url)), "..", "package.json");
6080
6474
  return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
6081
6475
  } catch {
6082
6476
  return "0.0.0";
@@ -6166,7 +6560,7 @@ function handleServeRequest(req, apiKey) {
6166
6560
  return Promise.resolve(json({ version: packageVersion() }));
6167
6561
  }
6168
6562
  if (url.pathname === "/openapi.json" && req.method === "GET") {
6169
- return Promise.resolve(json(openApiDocument));
6563
+ return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
6170
6564
  }
6171
6565
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
6172
6566
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
@@ -6238,7 +6632,7 @@ function startServeServer(options) {
6238
6632
  hostname: host,
6239
6633
  port,
6240
6634
  async fetch(req) {
6241
- return handleServeRequest(req, resolveHooksServePublishKey());
6635
+ return handleServeRequest(req, await resolveHooksServePublishKeyAsync());
6242
6636
  }
6243
6637
  });
6244
6638
  console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);