@hasna/hooks 0.7.9 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +483 -177
- package/bin/serve.js +281 -82
- package/dist/db/index.d.ts +3 -1
- package/dist/index.js +334 -131
- package/dist/lib/app-home.d.ts +65 -0
- package/dist/lib/profiles.d.ts +4 -1
- package/dist/serve.d.ts +7 -0
- package/dist/storage.js +148 -25
- package/package.json +6 -4
- package/scripts/artifact-scan.ts +49 -0
- package/scripts/ensure-profiles-dir.mjs +48 -0
- package/scripts/validate-package.ts +280 -0
package/bin/serve.js
CHANGED
|
@@ -15,6 +15,128 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// src/lib/app-home.ts
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { homedir as homedir2 } from "os";
|
|
21
|
+
import { join as join2, resolve } from "path";
|
|
22
|
+
|
|
23
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
var KIND_ENV = {
|
|
27
|
+
config: "HASNA_CONFIG_HOME",
|
|
28
|
+
data: "HASNA_DATA_HOME",
|
|
29
|
+
state: "HASNA_STATE_HOME",
|
|
30
|
+
cache: "HASNA_CACHE_HOME"
|
|
31
|
+
};
|
|
32
|
+
var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
33
|
+
function assertApp(app) {
|
|
34
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
35
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
36
|
+
}
|
|
37
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
38
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function envOf(options) {
|
|
42
|
+
return options.env ?? process.env;
|
|
43
|
+
}
|
|
44
|
+
function envValue(options, kind) {
|
|
45
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
46
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
function isMacOS(platform) {
|
|
49
|
+
return platform === "darwin";
|
|
50
|
+
}
|
|
51
|
+
function baseDir(kind, options) {
|
|
52
|
+
const override = envValue(options, kind);
|
|
53
|
+
if (override)
|
|
54
|
+
return override;
|
|
55
|
+
const home = options.home ?? homedir();
|
|
56
|
+
const platform = options.platform ?? process.platform;
|
|
57
|
+
if (isMacOS(platform)) {
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "config":
|
|
60
|
+
case "data":
|
|
61
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
62
|
+
case "cache":
|
|
63
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
64
|
+
case "state":
|
|
65
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
switch (kind) {
|
|
69
|
+
case "config":
|
|
70
|
+
return join(home, ".config", "hasna");
|
|
71
|
+
case "data":
|
|
72
|
+
return join(home, ".local", "share", "hasna");
|
|
73
|
+
case "state":
|
|
74
|
+
return join(home, ".local", "state", "hasna");
|
|
75
|
+
case "cache":
|
|
76
|
+
return join(home, ".cache", "hasna");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolvePath(kind, options) {
|
|
80
|
+
assertApp(options.app);
|
|
81
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
82
|
+
return join(baseDir(kind, options), appSegment);
|
|
83
|
+
}
|
|
84
|
+
function dataDir(options) {
|
|
85
|
+
return resolvePath("data", options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/lib/app-home.ts
|
|
89
|
+
var STORE_MARKER = "hooks.db";
|
|
90
|
+
function getHomeDir(env = process.env) {
|
|
91
|
+
return env.HOME || env.USERPROFILE || homedir2();
|
|
92
|
+
}
|
|
93
|
+
function getResolverDataRoot(env = process.env) {
|
|
94
|
+
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
95
|
+
}
|
|
96
|
+
function getLegacyDataRoot(env = process.env) {
|
|
97
|
+
return join2(getHomeDir(env), ".hasna", "hooks");
|
|
98
|
+
}
|
|
99
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
100
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
101
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
102
|
+
return true;
|
|
103
|
+
return existsSync(join2(resolved, STORE_MARKER));
|
|
104
|
+
}
|
|
105
|
+
function getExplicitDataDir(env = process.env) {
|
|
106
|
+
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
107
|
+
if (typeof dir === "string" && dir.trim().length > 0)
|
|
108
|
+
return dir.trim();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
function getExactDataRoot(env = process.env) {
|
|
112
|
+
const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
|
|
113
|
+
if (dir)
|
|
114
|
+
return resolve(dir);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
function getEffectiveDataRoot(env = process.env) {
|
|
118
|
+
const explicit = getExplicitDataDir(env);
|
|
119
|
+
if (explicit)
|
|
120
|
+
return explicit;
|
|
121
|
+
const exact = getExactDataRoot(env);
|
|
122
|
+
if (exact)
|
|
123
|
+
return exact;
|
|
124
|
+
const resolved = getResolverDataRoot(env);
|
|
125
|
+
return adoptResolverDataRoot(resolved, env) ? resolve(resolved) : getLegacyDataRoot(env);
|
|
126
|
+
}
|
|
127
|
+
function getReportedDbPath(env = process.env) {
|
|
128
|
+
const explicit = getExplicitDbPath(env);
|
|
129
|
+
if (explicit)
|
|
130
|
+
return explicit;
|
|
131
|
+
return join2(getEffectiveDataRoot(env), "hooks.db");
|
|
132
|
+
}
|
|
133
|
+
function getExplicitDbPath(env = process.env) {
|
|
134
|
+
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
135
|
+
if (typeof db === "string" && db.trim().length > 0)
|
|
136
|
+
return db.trim();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
18
140
|
// src/lib/registry.ts
|
|
19
141
|
var HOOKS = [
|
|
20
142
|
{
|
|
@@ -331,7 +453,7 @@ var HOOKS = [
|
|
|
331
453
|
{
|
|
332
454
|
name: "sessionlog",
|
|
333
455
|
displayName: "Session Log",
|
|
334
|
-
description:
|
|
456
|
+
description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
|
|
335
457
|
version: "0.1.0",
|
|
336
458
|
category: "Observability",
|
|
337
459
|
event: "PostToolUse",
|
|
@@ -341,7 +463,7 @@ var HOOKS = [
|
|
|
341
463
|
{
|
|
342
464
|
name: "commandlog",
|
|
343
465
|
displayName: "Command Log",
|
|
344
|
-
description:
|
|
466
|
+
description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
|
|
345
467
|
version: "0.1.0",
|
|
346
468
|
category: "Observability",
|
|
347
469
|
event: "PostToolUse",
|
|
@@ -361,7 +483,7 @@ var HOOKS = [
|
|
|
361
483
|
{
|
|
362
484
|
name: "errornotify",
|
|
363
485
|
displayName: "Error Notify",
|
|
364
|
-
description:
|
|
486
|
+
description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
|
|
365
487
|
version: "0.1.0",
|
|
366
488
|
category: "Observability",
|
|
367
489
|
event: "PostToolUse",
|
|
@@ -534,8 +656,8 @@ function getHook(name) {
|
|
|
534
656
|
}
|
|
535
657
|
|
|
536
658
|
// src/lib/manifest.ts
|
|
537
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
538
|
-
import { basename, dirname, isAbsolute, join as
|
|
659
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
660
|
+
import { basename, dirname, isAbsolute, join as join5, normalize, resolve as resolve2, sep } from "path";
|
|
539
661
|
|
|
540
662
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
541
663
|
var exports_external = {};
|
|
@@ -4512,7 +4634,8 @@ var coerce = {
|
|
|
4512
4634
|
var NEVER = INVALID;
|
|
4513
4635
|
// ../contracts/dist/client/transport.js
|
|
4514
4636
|
import { readFileSync, statSync } from "fs";
|
|
4515
|
-
import {
|
|
4637
|
+
import { createRequire } from "module";
|
|
4638
|
+
import { join as join3 } from "path";
|
|
4516
4639
|
function envToken(name) {
|
|
4517
4640
|
return name.toUpperCase().replace(/-/g, "_");
|
|
4518
4641
|
}
|
|
@@ -4527,6 +4650,9 @@ function credentialOverrideEnvKey(name) {
|
|
|
4527
4650
|
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
4528
4651
|
}
|
|
4529
4652
|
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
4653
|
+
function credentialPointerEnvKey(name) {
|
|
4654
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
4655
|
+
}
|
|
4530
4656
|
|
|
4531
4657
|
class CredentialResolutionError extends Error {
|
|
4532
4658
|
appName;
|
|
@@ -4539,31 +4665,55 @@ class CredentialResolutionError extends Error {
|
|
|
4539
4665
|
}
|
|
4540
4666
|
}
|
|
4541
4667
|
var HASNA_STATE_DIR = ".hasna";
|
|
4542
|
-
var FLEET_CREDENTIAL_DIR = "
|
|
4668
|
+
var FLEET_CREDENTIAL_DIR = "fleet-env";
|
|
4669
|
+
var LEGACY_CLOUD_DIR = "cloud";
|
|
4543
4670
|
var CONFIG_DIR = ".config";
|
|
4544
4671
|
var CONFIG_NAMESPACE = "hasna";
|
|
4672
|
+
var LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01";
|
|
4545
4673
|
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4546
4674
|
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4547
4675
|
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4548
4676
|
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4677
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
4549
4678
|
function homeDir(env) {
|
|
4550
4679
|
const home = env.HOME?.trim();
|
|
4551
4680
|
return home ? home : null;
|
|
4552
4681
|
}
|
|
4553
|
-
function
|
|
4554
|
-
return profileDiskSources(name, env, null);
|
|
4555
|
-
}
|
|
4556
|
-
function profileDiskSources(name, env, profile) {
|
|
4682
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
4557
4683
|
const home = homeDir(env);
|
|
4558
4684
|
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4559
4685
|
return [];
|
|
4560
4686
|
const stem = profile ? `${name}.${profile}` : name;
|
|
4561
4687
|
const configStem = profile ? `${name}-${profile}` : name;
|
|
4562
4688
|
return [
|
|
4563
|
-
|
|
4564
|
-
|
|
4689
|
+
{
|
|
4690
|
+
path: join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4691
|
+
tier: "fleet-env",
|
|
4692
|
+
deprecated: false
|
|
4693
|
+
},
|
|
4694
|
+
{
|
|
4695
|
+
path: join3(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
|
|
4696
|
+
tier: "legacy-cloud",
|
|
4697
|
+
deprecated: true
|
|
4698
|
+
},
|
|
4699
|
+
{
|
|
4700
|
+
path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
|
|
4701
|
+
tier: "config",
|
|
4702
|
+
deprecated: false
|
|
4703
|
+
},
|
|
4704
|
+
{
|
|
4705
|
+
path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
|
|
4706
|
+
tier: "config-legacy",
|
|
4707
|
+
deprecated: true
|
|
4708
|
+
}
|
|
4565
4709
|
];
|
|
4566
4710
|
}
|
|
4711
|
+
function credentialDiskSources(name, env) {
|
|
4712
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
4713
|
+
}
|
|
4714
|
+
function profileDiskSources(name, env, profile) {
|
|
4715
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
4716
|
+
}
|
|
4567
4717
|
function parseEnvFile(text) {
|
|
4568
4718
|
const values = new Map;
|
|
4569
4719
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -4614,6 +4764,9 @@ function readCredentialFile(path, apiKeyKeys) {
|
|
|
4614
4764
|
return null;
|
|
4615
4765
|
}
|
|
4616
4766
|
function assertUsableCredential(appName, source, value) {
|
|
4767
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
4768
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
4769
|
+
}
|
|
4617
4770
|
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
4618
4771
|
return;
|
|
4619
4772
|
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]);
|
|
@@ -4637,6 +4790,14 @@ function sealCredential(fields) {
|
|
|
4637
4790
|
writable: false,
|
|
4638
4791
|
configurable: false
|
|
4639
4792
|
});
|
|
4793
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
4794
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
4795
|
+
value: fields.pointerVaultKey,
|
|
4796
|
+
enumerable: false,
|
|
4797
|
+
writable: false,
|
|
4798
|
+
configurable: false
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4640
4801
|
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4641
4802
|
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4642
4803
|
enumerable: false,
|
|
@@ -4709,6 +4870,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4709
4870
|
warning: null
|
|
4710
4871
|
});
|
|
4711
4872
|
}
|
|
4873
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
4874
|
+
const pointerRaw = env[pointerKeyName];
|
|
4875
|
+
if (pointerRaw !== undefined) {
|
|
4876
|
+
const pointer = pointerRaw.trim();
|
|
4877
|
+
if (!pointer) {
|
|
4878
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
4879
|
+
}
|
|
4880
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
4881
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
4882
|
+
}
|
|
4883
|
+
return sealCredential({
|
|
4884
|
+
apiKey: "",
|
|
4885
|
+
pointerVaultKey: pointer,
|
|
4886
|
+
tier: "pointer",
|
|
4887
|
+
source: pointerKeyName,
|
|
4888
|
+
deliberate: true,
|
|
4889
|
+
deprecated: false,
|
|
4890
|
+
diskCandidates: diskPaths,
|
|
4891
|
+
warning: null
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4712
4894
|
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4713
4895
|
if (profile) {
|
|
4714
4896
|
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
@@ -4733,26 +4915,42 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4733
4915
|
}
|
|
4734
4916
|
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
4917
|
}
|
|
4736
|
-
const
|
|
4918
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
4919
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4737
4920
|
if (diskHits.length > 0) {
|
|
4738
4921
|
const winner = diskHits[0];
|
|
4739
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
4922
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
4740
4923
|
const divergentSources = [
|
|
4741
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
4924
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
4742
4925
|
...(() => {
|
|
4743
4926
|
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4744
4927
|
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4745
4928
|
})()
|
|
4746
4929
|
];
|
|
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;
|
|
4930
|
+
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;
|
|
4931
|
+
let deprecated = winner.src.deprecated;
|
|
4932
|
+
let finalWarning = warning;
|
|
4933
|
+
if (winner.src.deprecated) {
|
|
4934
|
+
deprecated = true;
|
|
4935
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4936
|
+
const notified = deprecationNotified();
|
|
4937
|
+
const noticeKey = `${name}:${winner.src.path}`;
|
|
4938
|
+
if (!notified.has(noticeKey)) {
|
|
4939
|
+
notified.add(noticeKey);
|
|
4940
|
+
const target = diskSourceList[0]?.path ?? "<none>";
|
|
4941
|
+
const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
|
|
4942
|
+
sink(message);
|
|
4943
|
+
}
|
|
4944
|
+
finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
|
|
4945
|
+
}
|
|
4748
4946
|
return sealCredential({
|
|
4749
4947
|
apiKey: winner.value,
|
|
4750
|
-
tier:
|
|
4751
|
-
source: winner.path,
|
|
4948
|
+
tier: winner.src.tier,
|
|
4949
|
+
source: winner.src.path,
|
|
4752
4950
|
deliberate: false,
|
|
4753
|
-
deprecated
|
|
4951
|
+
deprecated,
|
|
4754
4952
|
diskCandidates: diskPaths,
|
|
4755
|
-
warning
|
|
4953
|
+
warning: finalWarning
|
|
4756
4954
|
});
|
|
4757
4955
|
}
|
|
4758
4956
|
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
@@ -4778,6 +4976,8 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4778
4976
|
}
|
|
4779
4977
|
return null;
|
|
4780
4978
|
}
|
|
4979
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
4980
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
4781
4981
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4782
4982
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4783
4983
|
"host",
|
|
@@ -4788,22 +4988,18 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
4788
4988
|
]);
|
|
4789
4989
|
|
|
4790
4990
|
// src/config.ts
|
|
4791
|
-
import { join as
|
|
4792
|
-
import { homedir } from "os";
|
|
4991
|
+
import { join as join4 } from "path";
|
|
4793
4992
|
function getHooksDataDir() {
|
|
4794
|
-
|
|
4795
|
-
if (explicit)
|
|
4796
|
-
return explicit;
|
|
4797
|
-
return join2(homedir(), ".hasna", "hooks");
|
|
4993
|
+
return getEffectiveDataRoot();
|
|
4798
4994
|
}
|
|
4799
4995
|
function getCustomHooksDir() {
|
|
4800
|
-
return
|
|
4996
|
+
return join4(getHooksDataDir(), "hooks");
|
|
4801
4997
|
}
|
|
4802
4998
|
function getLockPath() {
|
|
4803
4999
|
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4804
5000
|
if (explicit)
|
|
4805
5001
|
return explicit;
|
|
4806
|
-
return
|
|
5002
|
+
return join4(getHooksDataDir(), "hooks.lock");
|
|
4807
5003
|
}
|
|
4808
5004
|
function resolveApiKey() {
|
|
4809
5005
|
return resolveCredential("hooks", process.env)?.apiKey;
|
|
@@ -4845,8 +5041,8 @@ function isWithin(child, parent) {
|
|
|
4845
5041
|
return child === parent || child.startsWith(parent + sep);
|
|
4846
5042
|
}
|
|
4847
5043
|
function assertContainedLexical(target, root, raw) {
|
|
4848
|
-
const lexical =
|
|
4849
|
-
const rootLexical =
|
|
5044
|
+
const lexical = resolve2(target);
|
|
5045
|
+
const rootLexical = resolve2(root);
|
|
4850
5046
|
if (!isWithin(lexical, rootLexical)) {
|
|
4851
5047
|
throw new ScriptContainmentError(target, root, raw);
|
|
4852
5048
|
}
|
|
@@ -4855,14 +5051,14 @@ function assertContainedLexical(target, root, raw) {
|
|
|
4855
5051
|
function realpathNearest(path) {
|
|
4856
5052
|
const tail = [];
|
|
4857
5053
|
let cur = path;
|
|
4858
|
-
while (!
|
|
5054
|
+
while (!existsSync2(cur)) {
|
|
4859
5055
|
const parent = dirname(cur);
|
|
4860
5056
|
if (parent === cur)
|
|
4861
5057
|
break;
|
|
4862
5058
|
tail.unshift(basename(cur));
|
|
4863
5059
|
cur = parent;
|
|
4864
5060
|
}
|
|
4865
|
-
return
|
|
5061
|
+
return join5(realpathSync(cur), ...tail);
|
|
4866
5062
|
}
|
|
4867
5063
|
function assertContainedReal(root, target, raw) {
|
|
4868
5064
|
const rootReal = realpathSync(root);
|
|
@@ -4895,25 +5091,25 @@ function resolveScript(manifest, manifestDir) {
|
|
|
4895
5091
|
if (isInlineScript(manifest))
|
|
4896
5092
|
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4897
5093
|
const rel = scriptRelFor(manifest);
|
|
4898
|
-
const resolved = assertContained(
|
|
4899
|
-
if (!
|
|
5094
|
+
const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
|
|
5095
|
+
if (!existsSync2(resolved)) {
|
|
4900
5096
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4901
5097
|
}
|
|
4902
5098
|
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4903
5099
|
}
|
|
4904
5100
|
function customHookDir(name) {
|
|
4905
|
-
return
|
|
5101
|
+
return join5(getCustomHooksDir(), shortManifestName(name));
|
|
4906
5102
|
}
|
|
4907
5103
|
function readCustomManifest(name) {
|
|
4908
5104
|
const dir = customHookDir(name);
|
|
4909
|
-
const manifestPath =
|
|
4910
|
-
if (!
|
|
5105
|
+
const manifestPath = join5(dir, "manifest.json");
|
|
5106
|
+
if (!existsSync2(manifestPath))
|
|
4911
5107
|
return;
|
|
4912
5108
|
try {
|
|
4913
5109
|
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4914
5110
|
const script = resolveScript(manifest, dir);
|
|
4915
5111
|
const scriptIsInline = isInlineScript(manifest);
|
|
4916
|
-
return { manifest, scriptPath:
|
|
5112
|
+
return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4917
5113
|
} catch (err) {
|
|
4918
5114
|
if (err instanceof ScriptContainmentError)
|
|
4919
5115
|
throw err;
|
|
@@ -4922,7 +5118,7 @@ function readCustomManifest(name) {
|
|
|
4922
5118
|
}
|
|
4923
5119
|
function listCustomHooks() {
|
|
4924
5120
|
const dir = getCustomHooksDir();
|
|
4925
|
-
if (!
|
|
5121
|
+
if (!existsSync2(dir))
|
|
4926
5122
|
return [];
|
|
4927
5123
|
const results = [];
|
|
4928
5124
|
for (const entry of readdirSafe(dir)) {
|
|
@@ -4942,14 +5138,13 @@ function readdirSafe(dir) {
|
|
|
4942
5138
|
|
|
4943
5139
|
// src/lib/store.ts
|
|
4944
5140
|
import { createHash } from "crypto";
|
|
4945
|
-
import { existsSync as
|
|
5141
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
4946
5142
|
import { readFile } from "fs/promises";
|
|
4947
5143
|
|
|
4948
5144
|
// src/db/index.ts
|
|
4949
5145
|
import { Database } from "bun:sqlite";
|
|
4950
|
-
import { existsSync as
|
|
4951
|
-
import { join as
|
|
4952
|
-
import { homedir as homedir3 } from "os";
|
|
5146
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
5147
|
+
import { join as join7 } from "path";
|
|
4953
5148
|
|
|
4954
5149
|
// src/db/schema.ts
|
|
4955
5150
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
@@ -5121,9 +5316,9 @@ function runMigrations(db) {
|
|
|
5121
5316
|
}
|
|
5122
5317
|
|
|
5123
5318
|
// src/db/legacy-import.ts
|
|
5124
|
-
import { existsSync as
|
|
5125
|
-
import { join as
|
|
5126
|
-
import { homedir as
|
|
5319
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
5320
|
+
import { join as join6 } from "path";
|
|
5321
|
+
import { homedir as homedir3 } from "os";
|
|
5127
5322
|
var META_KEY = "legacy_import_done";
|
|
5128
5323
|
function ensureMetaTable(db) {
|
|
5129
5324
|
db.exec(`
|
|
@@ -5198,25 +5393,25 @@ function importErrorsLog(db, filePath) {
|
|
|
5198
5393
|
} catch {}
|
|
5199
5394
|
return count;
|
|
5200
5395
|
}
|
|
5201
|
-
function runLegacyImport(db, homeDir2 =
|
|
5396
|
+
function runLegacyImport(db, homeDir2 = homedir3()) {
|
|
5202
5397
|
try {
|
|
5203
5398
|
if (isAlreadyDone(db))
|
|
5204
5399
|
return;
|
|
5205
5400
|
let total = 0;
|
|
5206
|
-
const claudeProjectsDir =
|
|
5207
|
-
if (
|
|
5401
|
+
const claudeProjectsDir = join6(homeDir2, ".claude", "projects");
|
|
5402
|
+
if (existsSync3(claudeProjectsDir)) {
|
|
5208
5403
|
try {
|
|
5209
5404
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
5210
5405
|
for (const dir of projectDirs) {
|
|
5211
|
-
const projectDir =
|
|
5406
|
+
const projectDir = join6(claudeProjectsDir, dir);
|
|
5212
5407
|
try {
|
|
5213
5408
|
const files = readdirSync2(projectDir);
|
|
5214
5409
|
for (const file of files) {
|
|
5215
5410
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
5216
|
-
total += importJsonlFile(db,
|
|
5411
|
+
total += importJsonlFile(db, join6(projectDir, file));
|
|
5217
5412
|
}
|
|
5218
5413
|
if (file === "errors.log") {
|
|
5219
|
-
total += importErrorsLog(db,
|
|
5414
|
+
total += importErrorsLog(db, join6(projectDir, file));
|
|
5220
5415
|
}
|
|
5221
5416
|
}
|
|
5222
5417
|
} catch {}
|
|
@@ -5251,27 +5446,24 @@ function runRetention(db, days) {
|
|
|
5251
5446
|
// src/db/index.ts
|
|
5252
5447
|
var instance = null;
|
|
5253
5448
|
function resolveDataDir() {
|
|
5254
|
-
const
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
if (!existsSync3(newDir) && existsSync3(oldDir)) {
|
|
5260
|
-
mkdirSync2(join5(homedir3(), ".hasna"), { recursive: true });
|
|
5261
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
5449
|
+
const effective = getEffectiveDataRoot();
|
|
5450
|
+
const oldDir = join7(getHomeDir(), ".hooks");
|
|
5451
|
+
if (!existsSync4(effective) && existsSync4(oldDir)) {
|
|
5452
|
+
mkdirSync2(effective, { recursive: true });
|
|
5453
|
+
cpSync(oldDir, effective, { recursive: true });
|
|
5262
5454
|
}
|
|
5263
|
-
return
|
|
5455
|
+
return effective;
|
|
5264
5456
|
}
|
|
5265
5457
|
function getDbPath() {
|
|
5266
5458
|
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
5267
5459
|
if (explicitDb)
|
|
5268
5460
|
return explicitDb;
|
|
5269
|
-
const
|
|
5270
|
-
return
|
|
5461
|
+
const dataDir2 = resolveDataDir();
|
|
5462
|
+
return join7(dataDir2, "hooks.db");
|
|
5271
5463
|
}
|
|
5272
5464
|
function ensureDir(dbPath) {
|
|
5273
5465
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5274
|
-
if (dir && !
|
|
5466
|
+
if (dir && !existsSync4(dir)) {
|
|
5275
5467
|
mkdirSync2(dir, { recursive: true });
|
|
5276
5468
|
}
|
|
5277
5469
|
}
|
|
@@ -5279,7 +5471,7 @@ function getDb() {
|
|
|
5279
5471
|
if (instance)
|
|
5280
5472
|
return instance;
|
|
5281
5473
|
const dbPath = getDbPath();
|
|
5282
|
-
const isNew = dbPath === ":memory:" || !
|
|
5474
|
+
const isNew = dbPath === ":memory:" || !existsSync4(dbPath);
|
|
5283
5475
|
ensureDir(dbPath);
|
|
5284
5476
|
instance = new Database(dbPath);
|
|
5285
5477
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -5362,7 +5554,7 @@ function assertLockShape(parsed, path) {
|
|
|
5362
5554
|
}
|
|
5363
5555
|
function readLock() {
|
|
5364
5556
|
const path = getLockPath();
|
|
5365
|
-
if (!
|
|
5557
|
+
if (!existsSync5(path))
|
|
5366
5558
|
return { hooks: {} };
|
|
5367
5559
|
let raw;
|
|
5368
5560
|
try {
|
|
@@ -5412,14 +5604,14 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
5412
5604
|
}
|
|
5413
5605
|
|
|
5414
5606
|
// src/lib/resolve.ts
|
|
5415
|
-
import { dirname as dirname2, join as
|
|
5416
|
-
import { existsSync as
|
|
5607
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
5608
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5417
5609
|
import { fileURLToPath } from "url";
|
|
5418
5610
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
5419
|
-
const sourceLayout =
|
|
5420
|
-
if (
|
|
5611
|
+
const sourceLayout = join8(moduleDir, "..", "..", "hooks");
|
|
5612
|
+
if (existsSync6(join8(sourceLayout, "hook-gitguard")))
|
|
5421
5613
|
return sourceLayout;
|
|
5422
|
-
return
|
|
5614
|
+
return join8(moduleDir, "..", "hooks");
|
|
5423
5615
|
}
|
|
5424
5616
|
function resolveHookDir(name) {
|
|
5425
5617
|
const custom2 = readCustomManifest(name);
|
|
@@ -5430,11 +5622,11 @@ function resolveHookDir(name) {
|
|
|
5430
5622
|
return;
|
|
5431
5623
|
const short = shortManifestName(name);
|
|
5432
5624
|
const bundledBase = resolveBundledHooksDir();
|
|
5433
|
-
const direct =
|
|
5434
|
-
if (
|
|
5625
|
+
const direct = join8(bundledBase, short);
|
|
5626
|
+
if (existsSync6(direct))
|
|
5435
5627
|
return direct;
|
|
5436
|
-
const prefixed =
|
|
5437
|
-
return
|
|
5628
|
+
const prefixed = join8(bundledBase, `hook-${short}`);
|
|
5629
|
+
return existsSync6(prefixed) ? prefixed : undefined;
|
|
5438
5630
|
}
|
|
5439
5631
|
function resolveScriptPath(name) {
|
|
5440
5632
|
const custom2 = readCustomManifest(name);
|
|
@@ -5443,8 +5635,8 @@ function resolveScriptPath(name) {
|
|
|
5443
5635
|
const dir = resolveHookDir(name);
|
|
5444
5636
|
if (!dir)
|
|
5445
5637
|
return;
|
|
5446
|
-
const script =
|
|
5447
|
-
return
|
|
5638
|
+
const script = join8(dir, "src", "hook.ts");
|
|
5639
|
+
return existsSync6(script) ? script : undefined;
|
|
5448
5640
|
}
|
|
5449
5641
|
function resolveHookMeta(name) {
|
|
5450
5642
|
const custom2 = readCustomManifest(name);
|
|
@@ -5497,7 +5689,7 @@ function resolveHook(name) {
|
|
|
5497
5689
|
|
|
5498
5690
|
// src/serve.ts
|
|
5499
5691
|
import { readFileSync as readFileSync5 } from "fs";
|
|
5500
|
-
import { dirname as dirname3, join as
|
|
5692
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
5501
5693
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5502
5694
|
|
|
5503
5695
|
// src/lib/secure-compare.ts
|
|
@@ -5670,7 +5862,7 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
5670
5862
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
5671
5863
|
function packageVersion() {
|
|
5672
5864
|
try {
|
|
5673
|
-
const pkgPath =
|
|
5865
|
+
const pkgPath = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
5674
5866
|
return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
|
|
5675
5867
|
} catch {
|
|
5676
5868
|
return "0.0.0";
|
|
@@ -5819,9 +6011,15 @@ function handleServeRequest(req, apiKey) {
|
|
|
5819
6011
|
}
|
|
5820
6012
|
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
5821
6013
|
}
|
|
6014
|
+
function resolveServeOptions(options) {
|
|
6015
|
+
const envPort = process.env.PORT?.trim();
|
|
6016
|
+
const envHost = process.env.HOST?.trim();
|
|
6017
|
+
const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
|
|
6018
|
+
const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
|
|
6019
|
+
return { port, host };
|
|
6020
|
+
}
|
|
5822
6021
|
function startServeServer(options) {
|
|
5823
|
-
const port = options
|
|
5824
|
-
const host = options.host ?? SERVE_HOST;
|
|
6022
|
+
const { port, host } = resolveServeOptions(options);
|
|
5825
6023
|
const apiKey = resolveApiKey();
|
|
5826
6024
|
const server = Bun.serve({
|
|
5827
6025
|
hostname: host,
|
|
@@ -5862,6 +6060,7 @@ options:
|
|
|
5862
6060
|
}
|
|
5863
6061
|
export {
|
|
5864
6062
|
startServeServer,
|
|
6063
|
+
resolveServeOptions,
|
|
5865
6064
|
handleServeRequest,
|
|
5866
6065
|
SERVE_SERVICE_NAME,
|
|
5867
6066
|
SERVE_HOST,
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite DB module for hooks — persistent storage at
|
|
2
|
+
* SQLite DB module for hooks — persistent storage at the effective data
|
|
3
|
+
* root resolved through @hasna/paths (legacy ~/.hasna/hooks until adopted,
|
|
4
|
+
* then the XDG data home) at hooks.db.
|
|
3
5
|
*
|
|
4
6
|
* Uses bun:sqlite with WAL mode for concurrent reads.
|
|
5
7
|
* Supports HASNA_HOOKS_DATA_DIR / HOOKS_DATA_DIR and HASNA_HOOKS_DB_PATH / HOOKS_DB_PATH env overrides.
|