@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.
- package/README.md +1 -1
- package/bin/index.js +1024 -461
- package/bin/serve.js +626 -158
- package/dist/config.d.ts +7 -5
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/index.js +898 -340
- package/dist/lib/hook-env.d.ts +60 -4
- package/dist/lib/manifest.d.ts +11 -0
- package/dist/lib/semver.d.ts +27 -0
- package/dist/openapi.d.ts +216 -0
- package/dist/storage.js +34 -34
- package/hooks/codewith-native-common.test.ts +25 -25
- package/hooks/codewith-native-common.ts +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-knowledge-context/README.md +1 -1
- package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-spiral-detector/LICENSE +191 -0
- package/hooks/hook-spiral-detector/README.md +23 -0
- package/hooks/hook-spiral-detector/package.json +12 -0
- package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
- package/hooks/hook-spiral-detector/src/hook.ts +93 -0
- package/hooks/hook-workspace-repos-guard/README.md +4 -3
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
- package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
- package/hooks/mention-context/src/hasna-mention-context.py +2 -2
- package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
- package/hooks/pre-bash/README.md +1 -1
- package/hooks/prompt-guard/README.md +1 -1
- package/hooks/session-start/README.md +1 -1
- package/hooks/stop-sync/README.md +1 -1
- package/hooks/worktree-guard/README.md +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
4797
|
+
return join2(homedir(), ".hasna", "hooks");
|
|
4511
4798
|
}
|
|
4512
4799
|
function getCustomHooksDir() {
|
|
4513
|
-
return
|
|
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
|
|
4806
|
+
return join2(getHooksDataDir(), "hooks.lock");
|
|
4520
4807
|
}
|
|
4521
4808
|
function resolveApiKey() {
|
|
4522
|
-
return
|
|
4809
|
+
return resolveCredential("hooks", process.env)?.apiKey;
|
|
4523
4810
|
}
|
|
4524
4811
|
|
|
4525
4812
|
// src/lib/semver.ts
|
|
4526
|
-
var
|
|
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
|
|
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(
|
|
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:
|
|
4902
|
+
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4613
4903
|
}
|
|
4614
4904
|
function customHookDir(name) {
|
|
4615
|
-
return
|
|
4905
|
+
return join3(getCustomHooksDir(), shortManifestName(name));
|
|
4616
4906
|
}
|
|
4617
4907
|
function readCustomManifest(name) {
|
|
4618
4908
|
const dir = customHookDir(name);
|
|
4619
|
-
const manifestPath =
|
|
4909
|
+
const manifestPath = join3(dir, "manifest.json");
|
|
4620
4910
|
if (!existsSync(manifestPath))
|
|
4621
4911
|
return;
|
|
4622
4912
|
try {
|
|
4623
|
-
const manifest = parseManifest(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
4835
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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,
|
|
5216
|
+
total += importJsonlFile(db, join4(projectDir, file));
|
|
4927
5217
|
}
|
|
4928
5218
|
if (file === "errors.log") {
|
|
4929
|
-
total += importErrorsLog(db,
|
|
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 =
|
|
4968
|
-
const oldDir =
|
|
5257
|
+
const newDir = join5(homedir3(), ".hasna", "hooks");
|
|
5258
|
+
const oldDir = join5(homedir3(), ".hooks");
|
|
4969
5259
|
if (!existsSync3(newDir) && existsSync3(oldDir)) {
|
|
4970
|
-
mkdirSync2(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
5130
|
-
if (existsSync5(
|
|
5419
|
+
const sourceLayout = join6(moduleDir, "..", "..", "hooks");
|
|
5420
|
+
if (existsSync5(join6(sourceLayout, "hook-gitguard")))
|
|
5131
5421
|
return sourceLayout;
|
|
5132
|
-
return
|
|
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 =
|
|
5433
|
+
const direct = join6(bundledBase, short);
|
|
5144
5434
|
if (existsSync5(direct))
|
|
5145
5435
|
return direct;
|
|
5146
|
-
const prefixed =
|
|
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 =
|
|
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
|
-
|
|
5383
|
-
handleServeRequest,
|
|
5384
|
-
SERVE_SERVICE_NAME,
|
|
5850
|
+
DEFAULT_SERVE_PORT,
|
|
5385
5851
|
SERVE_HOST,
|
|
5386
|
-
|
|
5852
|
+
SERVE_SERVICE_NAME,
|
|
5853
|
+
handleServeRequest,
|
|
5854
|
+
startServeServer
|
|
5387
5855
|
};
|