@hasna/hooks 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/bin/index.js +1024 -461
  3. package/bin/serve.js +626 -158
  4. package/dist/config.d.ts +7 -5
  5. package/dist/db/pg-migrations.d.ts +1 -1
  6. package/dist/index.js +898 -340
  7. package/dist/lib/hook-env.d.ts +60 -4
  8. package/dist/lib/manifest.d.ts +11 -0
  9. package/dist/lib/semver.d.ts +27 -0
  10. package/dist/openapi.d.ts +216 -0
  11. package/dist/storage.js +34 -34
  12. package/hooks/codewith-native-common.test.ts +25 -25
  13. package/hooks/codewith-native-common.ts +1 -1
  14. package/hooks/hook-autoformat/package.json +1 -1
  15. package/hooks/hook-desktopnotify/package.json +1 -1
  16. package/hooks/hook-envsetup/package.json +1 -1
  17. package/hooks/hook-knowledge-context/README.md +1 -1
  18. package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
  19. package/hooks/hook-permissionguard/package.json +1 -1
  20. package/hooks/hook-protectfiles/package.json +1 -1
  21. package/hooks/hook-spiral-detector/LICENSE +191 -0
  22. package/hooks/hook-spiral-detector/README.md +23 -0
  23. package/hooks/hook-spiral-detector/package.json +12 -0
  24. package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
  25. package/hooks/hook-spiral-detector/src/hook.ts +93 -0
  26. package/hooks/hook-workspace-repos-guard/README.md +4 -3
  27. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
  28. package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
  29. package/hooks/mention-context/src/hasna-mention-context.py +2 -2
  30. package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
  31. package/hooks/pre-bash/README.md +1 -1
  32. package/hooks/prompt-guard/README.md +1 -1
  33. package/hooks/session-start/README.md +1 -1
  34. package/hooks/stop-sync/README.md +1 -1
  35. package/hooks/worktree-guard/README.md +1 -1
  36. package/package.json +10 -6
package/bin/serve.js CHANGED
@@ -368,6 +368,16 @@ var HOOKS = [
368
368
  matcher: "",
369
369
  tags: ["errors", "failures", "logging", "debugging"]
370
370
  },
371
+ {
372
+ name: "spiral-detector",
373
+ displayName: "Spiral Detector",
374
+ description: "Interrupts sessions after five consecutive identical command failures",
375
+ version: "0.1.0",
376
+ category: "Observability",
377
+ event: "PostToolUse",
378
+ matcher: "",
379
+ tags: ["spiral", "failures", "retries", "safety", "interrupt"]
380
+ },
371
381
  {
372
382
  name: "stylescheck",
373
383
  displayName: "Styles Check",
@@ -524,122 +534,122 @@ function getHook(name) {
524
534
  }
525
535
 
526
536
  // src/lib/manifest.ts
527
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
528
- import { basename, dirname, isAbsolute, join as join2, normalize, resolve, sep } from "path";
537
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync } from "fs";
538
+ import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
529
539
 
530
- // node_modules/zod/v3/external.js
540
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
531
541
  var exports_external = {};
532
542
  __export(exports_external, {
533
- void: () => voidType,
534
- util: () => util,
535
- unknown: () => unknownType,
536
- union: () => unionType,
537
- undefined: () => undefinedType,
538
- tuple: () => tupleType,
539
- transformer: () => effectsType,
540
- symbol: () => symbolType,
541
- string: () => stringType,
542
- strictObject: () => strictObjectType,
543
- setErrorMap: () => setErrorMap,
544
- set: () => setType,
545
- record: () => recordType,
546
- quotelessJson: () => quotelessJson,
547
- promise: () => promiseType,
548
- preprocess: () => preprocessType,
549
- pipeline: () => pipelineType,
550
- ostring: () => ostring,
551
- optional: () => optionalType,
552
- onumber: () => onumber,
553
- oboolean: () => oboolean,
554
- objectUtil: () => objectUtil,
555
- object: () => objectType,
556
- number: () => numberType,
557
- nullable: () => nullableType,
558
- null: () => nullType,
559
- never: () => neverType,
560
- nativeEnum: () => nativeEnumType,
561
- nan: () => nanType,
562
- map: () => mapType,
563
- makeIssue: () => makeIssue,
564
- literal: () => literalType,
565
- lazy: () => lazyType,
566
- late: () => late,
567
- isValid: () => isValid,
568
- isDirty: () => isDirty,
569
- isAsync: () => isAsync,
570
- isAborted: () => isAborted,
571
- intersection: () => intersectionType,
572
- instanceof: () => instanceOfType,
573
- getParsedType: () => getParsedType,
574
- getErrorMap: () => getErrorMap,
575
- function: () => functionType,
576
- enum: () => enumType,
577
- effect: () => effectsType,
578
- discriminatedUnion: () => discriminatedUnionType,
579
- defaultErrorMap: () => en_default,
580
- datetimeRegex: () => datetimeRegex,
581
- date: () => dateType,
582
- custom: () => custom,
583
- coerce: () => coerce,
584
- boolean: () => booleanType,
585
- bigint: () => bigIntType,
586
- array: () => arrayType,
587
- any: () => anyType,
588
- addIssueToContext: () => addIssueToContext,
589
- ZodVoid: () => ZodVoid,
590
- ZodUnknown: () => ZodUnknown,
591
- ZodUnion: () => ZodUnion,
592
- ZodUndefined: () => ZodUndefined,
593
- ZodType: () => ZodType,
594
- ZodTuple: () => ZodTuple,
595
- ZodTransformer: () => ZodEffects,
596
- ZodSymbol: () => ZodSymbol,
597
- ZodString: () => ZodString,
598
- ZodSet: () => ZodSet,
599
- ZodSchema: () => ZodType,
600
- ZodRecord: () => ZodRecord,
601
- ZodReadonly: () => ZodReadonly,
602
- ZodPromise: () => ZodPromise,
603
- ZodPipeline: () => ZodPipeline,
604
- ZodParsedType: () => ZodParsedType,
605
- ZodOptional: () => ZodOptional,
606
- ZodObject: () => ZodObject,
607
- ZodNumber: () => ZodNumber,
608
- ZodNullable: () => ZodNullable,
609
- ZodNull: () => ZodNull,
610
- ZodNever: () => ZodNever,
611
- ZodNativeEnum: () => ZodNativeEnum,
612
- ZodNaN: () => ZodNaN,
613
- ZodMap: () => ZodMap,
614
- ZodLiteral: () => ZodLiteral,
615
- ZodLazy: () => ZodLazy,
616
- ZodIssueCode: () => ZodIssueCode,
617
- ZodIntersection: () => ZodIntersection,
618
- ZodFunction: () => ZodFunction,
619
- ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
620
- ZodError: () => ZodError,
621
- ZodEnum: () => ZodEnum,
622
- ZodEffects: () => ZodEffects,
623
- ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
624
- ZodDefault: () => ZodDefault,
625
- ZodDate: () => ZodDate,
626
- ZodCatch: () => ZodCatch,
627
- ZodBranded: () => ZodBranded,
628
- ZodBoolean: () => ZodBoolean,
629
- ZodBigInt: () => ZodBigInt,
630
- ZodArray: () => ZodArray,
631
- ZodAny: () => ZodAny,
632
- Schema: () => ZodType,
633
- ParseStatus: () => ParseStatus,
634
- OK: () => OK,
635
- NEVER: () => NEVER,
636
- INVALID: () => INVALID,
637
- EMPTY_PATH: () => EMPTY_PATH,
543
+ BRAND: () => BRAND,
638
544
  DIRTY: () => DIRTY,
639
- BRAND: () => BRAND
545
+ EMPTY_PATH: () => EMPTY_PATH,
546
+ INVALID: () => INVALID,
547
+ NEVER: () => NEVER,
548
+ OK: () => OK,
549
+ ParseStatus: () => ParseStatus,
550
+ Schema: () => ZodType,
551
+ ZodAny: () => ZodAny,
552
+ ZodArray: () => ZodArray,
553
+ ZodBigInt: () => ZodBigInt,
554
+ ZodBoolean: () => ZodBoolean,
555
+ ZodBranded: () => ZodBranded,
556
+ ZodCatch: () => ZodCatch,
557
+ ZodDate: () => ZodDate,
558
+ ZodDefault: () => ZodDefault,
559
+ ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
560
+ ZodEffects: () => ZodEffects,
561
+ ZodEnum: () => ZodEnum,
562
+ ZodError: () => ZodError,
563
+ ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
564
+ ZodFunction: () => ZodFunction,
565
+ ZodIntersection: () => ZodIntersection,
566
+ ZodIssueCode: () => ZodIssueCode,
567
+ ZodLazy: () => ZodLazy,
568
+ ZodLiteral: () => ZodLiteral,
569
+ ZodMap: () => ZodMap,
570
+ ZodNaN: () => ZodNaN,
571
+ ZodNativeEnum: () => ZodNativeEnum,
572
+ ZodNever: () => ZodNever,
573
+ ZodNull: () => ZodNull,
574
+ ZodNullable: () => ZodNullable,
575
+ ZodNumber: () => ZodNumber,
576
+ ZodObject: () => ZodObject,
577
+ ZodOptional: () => ZodOptional,
578
+ ZodParsedType: () => ZodParsedType,
579
+ ZodPipeline: () => ZodPipeline,
580
+ ZodPromise: () => ZodPromise,
581
+ ZodReadonly: () => ZodReadonly,
582
+ ZodRecord: () => ZodRecord,
583
+ ZodSchema: () => ZodType,
584
+ ZodSet: () => ZodSet,
585
+ ZodString: () => ZodString,
586
+ ZodSymbol: () => ZodSymbol,
587
+ ZodTransformer: () => ZodEffects,
588
+ ZodTuple: () => ZodTuple,
589
+ ZodType: () => ZodType,
590
+ ZodUndefined: () => ZodUndefined,
591
+ ZodUnion: () => ZodUnion,
592
+ ZodUnknown: () => ZodUnknown,
593
+ ZodVoid: () => ZodVoid,
594
+ addIssueToContext: () => addIssueToContext,
595
+ any: () => anyType,
596
+ array: () => arrayType,
597
+ bigint: () => bigIntType,
598
+ boolean: () => booleanType,
599
+ coerce: () => coerce,
600
+ custom: () => custom,
601
+ date: () => dateType,
602
+ datetimeRegex: () => datetimeRegex,
603
+ defaultErrorMap: () => en_default,
604
+ discriminatedUnion: () => discriminatedUnionType,
605
+ effect: () => effectsType,
606
+ enum: () => enumType,
607
+ function: () => functionType,
608
+ getErrorMap: () => getErrorMap,
609
+ getParsedType: () => getParsedType,
610
+ instanceof: () => instanceOfType,
611
+ intersection: () => intersectionType,
612
+ isAborted: () => isAborted,
613
+ isAsync: () => isAsync,
614
+ isDirty: () => isDirty,
615
+ isValid: () => isValid,
616
+ late: () => late,
617
+ lazy: () => lazyType,
618
+ literal: () => literalType,
619
+ makeIssue: () => makeIssue,
620
+ map: () => mapType,
621
+ nan: () => nanType,
622
+ nativeEnum: () => nativeEnumType,
623
+ never: () => neverType,
624
+ null: () => nullType,
625
+ nullable: () => nullableType,
626
+ number: () => numberType,
627
+ object: () => objectType,
628
+ objectUtil: () => objectUtil,
629
+ oboolean: () => oboolean,
630
+ onumber: () => onumber,
631
+ optional: () => optionalType,
632
+ ostring: () => ostring,
633
+ pipeline: () => pipelineType,
634
+ preprocess: () => preprocessType,
635
+ promise: () => promiseType,
636
+ quotelessJson: () => quotelessJson,
637
+ record: () => recordType,
638
+ set: () => setType,
639
+ setErrorMap: () => setErrorMap,
640
+ strictObject: () => strictObjectType,
641
+ string: () => stringType,
642
+ symbol: () => symbolType,
643
+ transformer: () => effectsType,
644
+ tuple: () => tupleType,
645
+ undefined: () => undefinedType,
646
+ union: () => unionType,
647
+ unknown: () => unknownType,
648
+ util: () => util,
649
+ void: () => voidType
640
650
  });
641
651
 
642
- // node_modules/zod/v3/helpers/util.js
652
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
643
653
  var util;
644
654
  (function(util2) {
645
655
  util2.assertEqual = (_) => {};
@@ -770,7 +780,7 @@ var getParsedType = (data) => {
770
780
  }
771
781
  };
772
782
 
773
- // node_modules/zod/v3/ZodError.js
783
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
774
784
  var ZodIssueCode = util.arrayToEnum([
775
785
  "invalid_type",
776
786
  "invalid_literal",
@@ -889,7 +899,7 @@ ZodError.create = (issues) => {
889
899
  return error;
890
900
  };
891
901
 
892
- // node_modules/zod/v3/locales/en.js
902
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
893
903
  var errorMap = (issue, _ctx) => {
894
904
  let message;
895
905
  switch (issue.code) {
@@ -992,7 +1002,7 @@ var errorMap = (issue, _ctx) => {
992
1002
  };
993
1003
  var en_default = errorMap;
994
1004
 
995
- // node_modules/zod/v3/errors.js
1005
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
996
1006
  var overrideErrorMap = en_default;
997
1007
  function setErrorMap(map) {
998
1008
  overrideErrorMap = map;
@@ -1000,7 +1010,7 @@ function setErrorMap(map) {
1000
1010
  function getErrorMap() {
1001
1011
  return overrideErrorMap;
1002
1012
  }
1003
- // node_modules/zod/v3/helpers/parseUtil.js
1013
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
1004
1014
  var makeIssue = (params) => {
1005
1015
  const { data, path, errorMaps, issueData } = params;
1006
1016
  const fullPath = [...path, ...issueData.path || []];
@@ -1106,14 +1116,14 @@ var isAborted = (x) => x.status === "aborted";
1106
1116
  var isDirty = (x) => x.status === "dirty";
1107
1117
  var isValid = (x) => x.status === "valid";
1108
1118
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
1109
- // node_modules/zod/v3/helpers/errorUtil.js
1119
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1110
1120
  var errorUtil;
1111
1121
  (function(errorUtil2) {
1112
1122
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
1113
1123
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
1114
1124
  })(errorUtil || (errorUtil = {}));
1115
1125
 
1116
- // node_modules/zod/v3/types.js
1126
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1117
1127
  class ParseInputLazyPath {
1118
1128
  constructor(parent, value, path, key) {
1119
1129
  this._cachedPath = [];
@@ -4500,30 +4510,309 @@ var coerce = {
4500
4510
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
4501
4511
  };
4502
4512
  var NEVER = INVALID;
4503
- // src/config.ts
4513
+ // ../contracts/dist/client/transport.js
4514
+ import { readFileSync, statSync } from "fs";
4504
4515
  import { join } from "path";
4516
+ function envToken(name) {
4517
+ return name.toUpperCase().replace(/-/g, "_");
4518
+ }
4519
+ function clientTransportEnvKeys(name) {
4520
+ const envSegment = envToken(name);
4521
+ return {
4522
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
4523
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
4524
+ };
4525
+ }
4526
+ function credentialOverrideEnvKey(name) {
4527
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4528
+ }
4529
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
4530
+
4531
+ class CredentialResolutionError extends Error {
4532
+ appName;
4533
+ attempted;
4534
+ constructor(appName, message, attempted) {
4535
+ super(message);
4536
+ this.name = "CredentialResolutionError";
4537
+ this.appName = appName;
4538
+ this.attempted = attempted;
4539
+ }
4540
+ }
4541
+ var HASNA_STATE_DIR = ".hasna";
4542
+ var FLEET_CREDENTIAL_DIR = "cloud";
4543
+ var CONFIG_DIR = ".config";
4544
+ var CONFIG_NAMESPACE = "hasna";
4545
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4546
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4547
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4548
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4549
+ function homeDir(env) {
4550
+ const home = env.HOME?.trim();
4551
+ return home ? home : null;
4552
+ }
4553
+ function credentialDiskSources(name, env) {
4554
+ return profileDiskSources(name, env, null);
4555
+ }
4556
+ function profileDiskSources(name, env, profile) {
4557
+ const home = homeDir(env);
4558
+ if (!home || !SAFE_APP_SLUG.test(name))
4559
+ return [];
4560
+ const stem = profile ? `${name}.${profile}` : name;
4561
+ const configStem = profile ? `${name}-${profile}` : name;
4562
+ return [
4563
+ join(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4564
+ join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
4565
+ ];
4566
+ }
4567
+ function parseEnvFile(text) {
4568
+ const values = new Map;
4569
+ for (const rawLine of text.split(/\r?\n/)) {
4570
+ const line = rawLine.trim();
4571
+ if (line.length === 0 || line.startsWith("#"))
4572
+ continue;
4573
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
4574
+ const equals = withoutExport.indexOf("=");
4575
+ if (equals <= 0)
4576
+ continue;
4577
+ const key = withoutExport.slice(0, equals).trim();
4578
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
4579
+ continue;
4580
+ let value = withoutExport.slice(equals + 1).trim();
4581
+ const quote = value[0];
4582
+ if (quote === '"' || quote === "'") {
4583
+ if (value.length < 2 || !value.endsWith(quote))
4584
+ continue;
4585
+ value = value.slice(1, -1);
4586
+ }
4587
+ if (value.length === 0)
4588
+ continue;
4589
+ values.set(key, value);
4590
+ }
4591
+ return values;
4592
+ }
4593
+ function readAppConfigFile(path) {
4594
+ let text;
4595
+ try {
4596
+ const stats = statSync(path);
4597
+ if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
4598
+ return null;
4599
+ text = readFileSync(path, "utf8");
4600
+ } catch {
4601
+ return null;
4602
+ }
4603
+ return parseEnvFile(text);
4604
+ }
4605
+ function readCredentialFile(path, apiKeyKeys) {
4606
+ const values = readAppConfigFile(path);
4607
+ if (!values)
4608
+ return null;
4609
+ for (const key of apiKeyKeys) {
4610
+ const value = values.get(key)?.trim();
4611
+ if (value)
4612
+ return value;
4613
+ }
4614
+ return null;
4615
+ }
4616
+ function assertUsableCredential(appName, source, value) {
4617
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4618
+ return;
4619
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
4620
+ }
4621
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
4622
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
4623
+ function sealCredential(fields) {
4624
+ const { apiKey } = fields;
4625
+ const visible = {
4626
+ tier: fields.tier,
4627
+ source: fields.source,
4628
+ deliberate: fields.deliberate,
4629
+ deprecated: fields.deprecated,
4630
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
4631
+ warning: fields.warning
4632
+ };
4633
+ const sealed = { ...visible };
4634
+ Object.defineProperty(sealed, "apiKey", {
4635
+ value: apiKey,
4636
+ enumerable: false,
4637
+ writable: false,
4638
+ configurable: false
4639
+ });
4640
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
4641
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
4642
+ enumerable: false,
4643
+ writable: false,
4644
+ configurable: false
4645
+ });
4646
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
4647
+ value: true,
4648
+ enumerable: false,
4649
+ writable: false,
4650
+ configurable: false
4651
+ });
4652
+ return Object.freeze(sealed);
4653
+ }
4654
+ function firstEnvValue(env, keys) {
4655
+ for (const key of keys) {
4656
+ const value = env[key]?.trim();
4657
+ if (value)
4658
+ return { key, value };
4659
+ }
4660
+ return null;
4661
+ }
4662
+ var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
4663
+ function deprecationNotified() {
4664
+ const host = globalThis;
4665
+ const existing = host[DEPRECATION_REGISTRY];
4666
+ if (existing instanceof Set)
4667
+ return existing;
4668
+ const created = new Set;
4669
+ host[DEPRECATION_REGISTRY] = created;
4670
+ return created;
4671
+ }
4672
+ function defaultDeprecationSink(message) {
4673
+ if (typeof process !== "undefined" && process.stderr) {
4674
+ process.stderr.write(`${message}
4675
+ `);
4676
+ }
4677
+ }
4678
+ function resolveCredential(name, env, options = {}) {
4679
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
4680
+ const diskPaths = credentialDiskSources(name, env);
4681
+ const explicitKey = options.apiKey?.trim();
4682
+ if (explicitKey) {
4683
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
4684
+ return sealCredential({
4685
+ apiKey: explicitKey,
4686
+ tier: "argument",
4687
+ source: "explicit apiKey argument",
4688
+ deliberate: true,
4689
+ deprecated: false,
4690
+ diskCandidates: diskPaths,
4691
+ warning: null
4692
+ });
4693
+ }
4694
+ const overrideKeyName = credentialOverrideEnvKey(name);
4695
+ const overrideRaw = env[overrideKeyName];
4696
+ if (overrideRaw !== undefined) {
4697
+ const override = overrideRaw.trim();
4698
+ if (!override) {
4699
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
4700
+ }
4701
+ assertUsableCredential(name, overrideKeyName, override);
4702
+ return sealCredential({
4703
+ apiKey: override,
4704
+ tier: "override",
4705
+ source: overrideKeyName,
4706
+ deliberate: true,
4707
+ deprecated: false,
4708
+ diskCandidates: diskPaths,
4709
+ warning: null
4710
+ });
4711
+ }
4712
+ const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4713
+ if (profile) {
4714
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
4715
+ if (!SAFE_PROFILE.test(profile)) {
4716
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
4717
+ }
4718
+ const paths = profileDiskSources(name, env, profile);
4719
+ for (const path of paths) {
4720
+ const value = readCredentialFile(path, apiKeyKeys);
4721
+ if (value) {
4722
+ assertUsableCredential(name, path, value);
4723
+ return sealCredential({
4724
+ apiKey: value,
4725
+ tier: "profile",
4726
+ source: path,
4727
+ deliberate: true,
4728
+ deprecated: false,
4729
+ diskCandidates: paths,
4730
+ warning: null
4731
+ });
4732
+ }
4733
+ }
4734
+ 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);
4735
+ }
4736
+ const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4737
+ if (diskHits.length > 0) {
4738
+ const winner = diskHits[0];
4739
+ assertUsableCredential(name, winner.path, winner.value);
4740
+ const divergentSources = [
4741
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
4742
+ ...(() => {
4743
+ const legacyHit = firstEnvValue(env, apiKeyKeys);
4744
+ return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4745
+ })()
4746
+ ];
4747
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
4748
+ return sealCredential({
4749
+ apiKey: winner.value,
4750
+ tier: "disk",
4751
+ source: winner.path,
4752
+ deliberate: false,
4753
+ deprecated: false,
4754
+ diskCandidates: diskPaths,
4755
+ warning
4756
+ });
4757
+ }
4758
+ const legacy = firstEnvValue(env, apiKeyKeys);
4759
+ if (legacy) {
4760
+ assertUsableCredential(name, legacy.key, legacy.value);
4761
+ const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
4762
+ const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
4763
+ const sink = options.onDeprecation ?? defaultDeprecationSink;
4764
+ const notified = deprecationNotified();
4765
+ if (!notified.has(name)) {
4766
+ notified.add(name);
4767
+ sink(message);
4768
+ }
4769
+ return sealCredential({
4770
+ apiKey: legacy.value,
4771
+ tier: "legacy-env",
4772
+ source: legacy.key,
4773
+ deliberate: false,
4774
+ deprecated: true,
4775
+ diskCandidates: diskPaths,
4776
+ warning: message
4777
+ });
4778
+ }
4779
+ return null;
4780
+ }
4781
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4782
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
4783
+ "host",
4784
+ ":authority",
4785
+ "forwarded",
4786
+ "x-forwarded-host",
4787
+ "x-original-host"
4788
+ ]);
4789
+
4790
+ // src/config.ts
4791
+ import { join as join2 } from "path";
4505
4792
  import { homedir } from "os";
4506
4793
  function getHooksDataDir() {
4507
4794
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4508
4795
  if (explicit)
4509
4796
  return explicit;
4510
- return join(homedir(), ".hasna", "hooks");
4797
+ return join2(homedir(), ".hasna", "hooks");
4511
4798
  }
4512
4799
  function getCustomHooksDir() {
4513
- return join(getHooksDataDir(), "hooks");
4800
+ return join2(getHooksDataDir(), "hooks");
4514
4801
  }
4515
4802
  function getLockPath() {
4516
4803
  const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4517
4804
  if (explicit)
4518
4805
  return explicit;
4519
- return join(getHooksDataDir(), "hooks.lock");
4806
+ return join2(getHooksDataDir(), "hooks.lock");
4520
4807
  }
4521
4808
  function resolveApiKey() {
4522
- return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4809
+ return resolveCredential("hooks", process.env)?.apiKey;
4523
4810
  }
4524
4811
 
4525
4812
  // src/lib/semver.ts
4526
- var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4813
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})";
4814
+ var ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*";
4815
+ var SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4527
4816
 
4528
4817
  // src/lib/manifest.ts
4529
4818
  var HOOK_NAME_RE = /^[\w-]+$/;
@@ -4535,7 +4824,8 @@ var manifestSchema = exports_external.object({
4535
4824
  script: exports_external.string().min(1, "script must name a relative path or inline content"),
4536
4825
  script_kind: exports_external.enum(["inline", "file"]).optional(),
4537
4826
  args: exports_external.array(exports_external.string()).optional(),
4538
- timeout_ms: exports_external.number().int().positive().optional()
4827
+ timeout_ms: exports_external.number().int().positive().optional(),
4828
+ env: exports_external.record(exports_external.string(), exports_external.string()).optional()
4539
4829
  });
4540
4830
  function parseManifest(json) {
4541
4831
  const parsed = JSON.parse(json);
@@ -4572,7 +4862,7 @@ function realpathNearest(path) {
4572
4862
  tail.unshift(basename(cur));
4573
4863
  cur = parent;
4574
4864
  }
4575
- return join2(realpathSync(cur), ...tail);
4865
+ return join3(realpathSync(cur), ...tail);
4576
4866
  }
4577
4867
  function assertContainedReal(root, target, raw) {
4578
4868
  const rootReal = realpathSync(root);
@@ -4605,25 +4895,25 @@ function resolveScript(manifest, manifestDir) {
4605
4895
  if (isInlineScript(manifest))
4606
4896
  return { path: scriptRelFor(manifest), content: manifest.script };
4607
4897
  const rel = scriptRelFor(manifest);
4608
- const resolved = assertContained(join2(manifestDir, rel), manifestDir, manifest.script);
4898
+ const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
4609
4899
  if (!existsSync(resolved)) {
4610
4900
  throw new Error(`manifest script file not found: ${resolved}`);
4611
4901
  }
4612
- return { path: rel, content: readFileSync(resolved, "utf-8") };
4902
+ return { path: rel, content: readFileSync2(resolved, "utf-8") };
4613
4903
  }
4614
4904
  function customHookDir(name) {
4615
- return join2(getCustomHooksDir(), shortManifestName(name));
4905
+ return join3(getCustomHooksDir(), shortManifestName(name));
4616
4906
  }
4617
4907
  function readCustomManifest(name) {
4618
4908
  const dir = customHookDir(name);
4619
- const manifestPath = join2(dir, "manifest.json");
4909
+ const manifestPath = join3(dir, "manifest.json");
4620
4910
  if (!existsSync(manifestPath))
4621
4911
  return;
4622
4912
  try {
4623
- const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
4913
+ const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4624
4914
  const script = resolveScript(manifest, dir);
4625
4915
  const scriptIsInline = isInlineScript(manifest);
4626
- return { manifest, scriptPath: join2(dir, script.path), scriptContent: script.content, scriptIsInline };
4916
+ return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
4627
4917
  } catch (err) {
4628
4918
  if (err instanceof ScriptContainmentError)
4629
4919
  throw err;
@@ -4652,13 +4942,13 @@ function readdirSafe(dir) {
4652
4942
 
4653
4943
  // src/lib/store.ts
4654
4944
  import { createHash } from "crypto";
4655
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
4945
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
4656
4946
  import { readFile } from "fs/promises";
4657
4947
 
4658
4948
  // src/db/index.ts
4659
4949
  import { Database } from "bun:sqlite";
4660
4950
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
4661
- import { join as join4 } from "path";
4951
+ import { join as join5 } from "path";
4662
4952
  import { homedir as homedir3 } from "os";
4663
4953
 
4664
4954
  // src/db/schema.ts
@@ -4831,8 +5121,8 @@ function runMigrations(db) {
4831
5121
  }
4832
5122
 
4833
5123
  // src/db/legacy-import.ts
4834
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
4835
- import { join as join3 } from "path";
5124
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5125
+ import { join as join4 } from "path";
4836
5126
  import { homedir as homedir2 } from "os";
4837
5127
  var META_KEY = "legacy_import_done";
4838
5128
  function ensureMetaTable(db) {
@@ -4857,7 +5147,7 @@ function nanoid() {
4857
5147
  function importJsonlFile(db, filePath) {
4858
5148
  let count = 0;
4859
5149
  try {
4860
- const lines = readFileSync2(filePath, "utf-8").split(`
5150
+ const lines = readFileSync3(filePath, "utf-8").split(`
4861
5151
  `).filter(Boolean);
4862
5152
  for (const line of lines) {
4863
5153
  try {
@@ -4883,7 +5173,7 @@ function importJsonlFile(db, filePath) {
4883
5173
  function importErrorsLog(db, filePath) {
4884
5174
  let count = 0;
4885
5175
  try {
4886
- const lines = readFileSync2(filePath, "utf-8").split(`
5176
+ const lines = readFileSync3(filePath, "utf-8").split(`
4887
5177
  `).filter(Boolean);
4888
5178
  const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
4889
5179
  for (const line of lines) {
@@ -4908,25 +5198,25 @@ function importErrorsLog(db, filePath) {
4908
5198
  } catch {}
4909
5199
  return count;
4910
5200
  }
4911
- function runLegacyImport(db, homeDir = homedir2()) {
5201
+ function runLegacyImport(db, homeDir2 = homedir2()) {
4912
5202
  try {
4913
5203
  if (isAlreadyDone(db))
4914
5204
  return;
4915
5205
  let total = 0;
4916
- const claudeProjectsDir = join3(homeDir, ".claude", "projects");
5206
+ const claudeProjectsDir = join4(homeDir2, ".claude", "projects");
4917
5207
  if (existsSync2(claudeProjectsDir)) {
4918
5208
  try {
4919
5209
  const projectDirs = readdirSync2(claudeProjectsDir);
4920
5210
  for (const dir of projectDirs) {
4921
- const projectDir = join3(claudeProjectsDir, dir);
5211
+ const projectDir = join4(claudeProjectsDir, dir);
4922
5212
  try {
4923
5213
  const files = readdirSync2(projectDir);
4924
5214
  for (const file of files) {
4925
5215
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
4926
- total += importJsonlFile(db, join3(projectDir, file));
5216
+ total += importJsonlFile(db, join4(projectDir, file));
4927
5217
  }
4928
5218
  if (file === "errors.log") {
4929
- total += importErrorsLog(db, join3(projectDir, file));
5219
+ total += importErrorsLog(db, join4(projectDir, file));
4930
5220
  }
4931
5221
  }
4932
5222
  } catch {}
@@ -4964,10 +5254,10 @@ function resolveDataDir() {
4964
5254
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4965
5255
  if (explicit)
4966
5256
  return explicit;
4967
- const newDir = join4(homedir3(), ".hasna", "hooks");
4968
- const oldDir = join4(homedir3(), ".hooks");
5257
+ const newDir = join5(homedir3(), ".hasna", "hooks");
5258
+ const oldDir = join5(homedir3(), ".hooks");
4969
5259
  if (!existsSync3(newDir) && existsSync3(oldDir)) {
4970
- mkdirSync2(join4(homedir3(), ".hasna"), { recursive: true });
5260
+ mkdirSync2(join5(homedir3(), ".hasna"), { recursive: true });
4971
5261
  cpSync(oldDir, newDir, { recursive: true });
4972
5262
  }
4973
5263
  return newDir;
@@ -4977,7 +5267,7 @@ function getDbPath() {
4977
5267
  if (explicitDb)
4978
5268
  return explicitDb;
4979
5269
  const dataDir = resolveDataDir();
4980
- return join4(dataDir, "hooks.db");
5270
+ return join5(dataDir, "hooks.db");
4981
5271
  }
4982
5272
  function ensureDir(dbPath) {
4983
5273
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
@@ -5076,7 +5366,7 @@ function readLock() {
5076
5366
  return { hooks: {} };
5077
5367
  let raw;
5078
5368
  try {
5079
- raw = readFileSync3(path, "utf-8");
5369
+ raw = readFileSync4(path, "utf-8");
5080
5370
  } catch {
5081
5371
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
5082
5372
  }
@@ -5108,7 +5398,7 @@ function setPinnedHook(name, entry) {
5108
5398
  return writeLock(lock);
5109
5399
  }
5110
5400
  function retrustHook(name, scriptPath, version, source) {
5111
- const actual = sha256Of(readFileSync3(scriptPath));
5401
+ const actual = sha256Of(readFileSync4(scriptPath));
5112
5402
  const db = getDb();
5113
5403
  upsertHookRecord(db, {
5114
5404
  name,
@@ -5122,14 +5412,14 @@ function retrustHook(name, scriptPath, version, source) {
5122
5412
  }
5123
5413
 
5124
5414
  // src/lib/resolve.ts
5125
- import { dirname as dirname2, join as join5 } from "path";
5415
+ import { dirname as dirname2, join as join6 } from "path";
5126
5416
  import { existsSync as existsSync5 } from "fs";
5127
5417
  import { fileURLToPath } from "url";
5128
5418
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
5129
- const sourceLayout = join5(moduleDir, "..", "..", "hooks");
5130
- if (existsSync5(join5(sourceLayout, "hook-gitguard")))
5419
+ const sourceLayout = join6(moduleDir, "..", "..", "hooks");
5420
+ if (existsSync5(join6(sourceLayout, "hook-gitguard")))
5131
5421
  return sourceLayout;
5132
- return join5(moduleDir, "..", "hooks");
5422
+ return join6(moduleDir, "..", "hooks");
5133
5423
  }
5134
5424
  function resolveHookDir(name) {
5135
5425
  const custom2 = readCustomManifest(name);
@@ -5140,10 +5430,10 @@ function resolveHookDir(name) {
5140
5430
  return;
5141
5431
  const short = shortManifestName(name);
5142
5432
  const bundledBase = resolveBundledHooksDir();
5143
- const direct = join5(bundledBase, short);
5433
+ const direct = join6(bundledBase, short);
5144
5434
  if (existsSync5(direct))
5145
5435
  return direct;
5146
- const prefixed = join5(bundledBase, `hook-${short}`);
5436
+ const prefixed = join6(bundledBase, `hook-${short}`);
5147
5437
  return existsSync5(prefixed) ? prefixed : undefined;
5148
5438
  }
5149
5439
  function resolveScriptPath(name) {
@@ -5153,7 +5443,7 @@ function resolveScriptPath(name) {
5153
5443
  const dir = resolveHookDir(name);
5154
5444
  if (!dir)
5155
5445
  return;
5156
- const script = join5(dir, "src", "hook.ts");
5446
+ const script = join6(dir, "src", "hook.ts");
5157
5447
  return existsSync5(script) ? script : undefined;
5158
5448
  }
5159
5449
  function resolveHookMeta(name) {
@@ -5205,6 +5495,11 @@ function resolveHook(name) {
5205
5495
  };
5206
5496
  }
5207
5497
 
5498
+ // src/serve.ts
5499
+ import { readFileSync as readFileSync5 } from "fs";
5500
+ import { dirname as dirname3, join as join7 } from "path";
5501
+ import { fileURLToPath as fileURLToPath2 } from "url";
5502
+
5208
5503
  // src/lib/secure-compare.ts
5209
5504
  function secureEqual(a, b) {
5210
5505
  const enc = new TextEncoder;
@@ -5218,10 +5513,169 @@ function secureEqual(a, b) {
5218
5513
  return diff === 0;
5219
5514
  }
5220
5515
 
5516
+ // src/openapi.ts
5517
+ var openApiDocument = {
5518
+ openapi: "3.1.0",
5519
+ info: {
5520
+ title: "Hooks Registry API",
5521
+ version: "0.1.0",
5522
+ 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)."
5523
+ },
5524
+ servers: [{ url: "/" }],
5525
+ paths: {
5526
+ "/health": {
5527
+ get: {
5528
+ summary: "Liveness probe",
5529
+ operationId: "getHealth",
5530
+ responses: {
5531
+ "200": {
5532
+ description: "Service is alive",
5533
+ content: {
5534
+ "application/json": {
5535
+ schema: { type: "object", additionalProperties: true }
5536
+ }
5537
+ }
5538
+ }
5539
+ }
5540
+ }
5541
+ },
5542
+ "/ready": {
5543
+ get: {
5544
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
5545
+ operationId: "getReady",
5546
+ responses: {
5547
+ "200": { description: "Lock store is readable" },
5548
+ "503": { description: "Lock store is not readable" }
5549
+ }
5550
+ }
5551
+ },
5552
+ "/version": {
5553
+ get: {
5554
+ summary: "Package version",
5555
+ operationId: "getVersion",
5556
+ responses: {
5557
+ "200": {
5558
+ description: "The installed @hasna/hooks version",
5559
+ content: {
5560
+ "application/json": {
5561
+ schema: { type: "object", properties: { version: { type: "string" } } }
5562
+ }
5563
+ }
5564
+ }
5565
+ }
5566
+ }
5567
+ },
5568
+ "/openapi.json": {
5569
+ get: {
5570
+ summary: "This OpenAPI document",
5571
+ operationId: "getOpenApi",
5572
+ responses: { "200": { description: "OpenAPI 3.1 document" } }
5573
+ }
5574
+ },
5575
+ "/api/v1/catalog": {
5576
+ get: {
5577
+ summary: "List the full hook catalog with versions and sha256",
5578
+ operationId: "getCatalog",
5579
+ responses: {
5580
+ "200": {
5581
+ description: "Catalog entries",
5582
+ content: {
5583
+ "application/json": {
5584
+ schema: {
5585
+ type: "object",
5586
+ properties: {
5587
+ hooks: {
5588
+ type: "array",
5589
+ items: {
5590
+ type: "object",
5591
+ properties: {
5592
+ name: { type: "string" },
5593
+ version: { type: "string" },
5594
+ sha256: { type: "string" },
5595
+ events: { type: "array", items: { type: "string" } }
5596
+ }
5597
+ }
5598
+ }
5599
+ }
5600
+ }
5601
+ }
5602
+ }
5603
+ }
5604
+ }
5605
+ }
5606
+ },
5607
+ "/api/v1/lock": {
5608
+ get: {
5609
+ summary: "Read the published hooks.lock with per-hook versions",
5610
+ operationId: "getLock",
5611
+ responses: { "200": { description: "The lock file" } }
5612
+ }
5613
+ },
5614
+ "/api/v1/hooks/{name}/{version}": {
5615
+ get: {
5616
+ summary: "Fetch one hook's manifest and script",
5617
+ operationId: "getHookArtifact",
5618
+ parameters: [
5619
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
5620
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
5621
+ ],
5622
+ responses: {
5623
+ "200": {
5624
+ description: "Hook artifact; x-hook-sha256 header carries the script hash"
5625
+ },
5626
+ "400": { description: "Invalid URL encoding or semver version" },
5627
+ "404": { description: "Hook not found locally" }
5628
+ }
5629
+ }
5630
+ },
5631
+ "/api/v1/hooks": {
5632
+ put: {
5633
+ summary: "Publish/retrust a hook (requires the API key)",
5634
+ operationId: "putHook",
5635
+ requestBody: {
5636
+ required: true,
5637
+ content: {
5638
+ "application/json": {
5639
+ schema: {
5640
+ type: "object",
5641
+ properties: { name: { type: "string" }, version: { type: "string" } }
5642
+ }
5643
+ }
5644
+ }
5645
+ },
5646
+ responses: {
5647
+ "200": { description: "Hook retrusted and pin updated" },
5648
+ "401": { description: "Missing or invalid API key" },
5649
+ "404": { description: "Hook not found in local store" },
5650
+ "409": { description: "Version mismatch with the local store" }
5651
+ },
5652
+ security: [{ apiKey: [] }]
5653
+ }
5654
+ }
5655
+ },
5656
+ components: {
5657
+ securitySchemes: {
5658
+ apiKey: {
5659
+ type: "http",
5660
+ scheme: "bearer",
5661
+ description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
5662
+ }
5663
+ }
5664
+ }
5665
+ };
5666
+
5221
5667
  // src/serve.ts
5222
5668
  var DEFAULT_SERVE_PORT = 39428;
5223
5669
  var SERVE_HOST = "127.0.0.1";
5224
5670
  var SERVE_SERVICE_NAME = "hooks-registry";
5671
+ function packageVersion() {
5672
+ try {
5673
+ const pkgPath = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
5674
+ return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
5675
+ } catch {
5676
+ return "0.0.0";
5677
+ }
5678
+ }
5225
5679
  async function buildCatalog() {
5226
5680
  const byName = new Map;
5227
5681
  for (const meta of HOOKS) {
@@ -5294,6 +5748,20 @@ function handleServeRequest(req, apiKey) {
5294
5748
  if (url.pathname === "/health" && req.method === "GET") {
5295
5749
  return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
5296
5750
  }
5751
+ if (url.pathname === "/ready" && req.method === "GET") {
5752
+ try {
5753
+ readLock();
5754
+ return Promise.resolve(json({ status: "ready", name: SERVE_SERVICE_NAME }));
5755
+ } catch {
5756
+ return Promise.resolve(json({ status: "not-ready", name: SERVE_SERVICE_NAME }, 503));
5757
+ }
5758
+ }
5759
+ if (url.pathname === "/version" && req.method === "GET") {
5760
+ return Promise.resolve(json({ version: packageVersion() }));
5761
+ }
5762
+ if (url.pathname === "/openapi.json" && req.method === "GET") {
5763
+ return Promise.resolve(json(openApiDocument));
5764
+ }
5297
5765
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
5298
5766
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
5299
5767
  }
@@ -5379,9 +5847,9 @@ if (import.meta.main) {
5379
5847
  });
5380
5848
  }
5381
5849
  export {
5382
- startServeServer,
5383
- handleServeRequest,
5384
- SERVE_SERVICE_NAME,
5850
+ DEFAULT_SERVE_PORT,
5385
5851
  SERVE_HOST,
5386
- DEFAULT_SERVE_PORT
5852
+ SERVE_SERVICE_NAME,
5853
+ handleServeRequest,
5854
+ startServeServer
5387
5855
  };