@hasna/hooks 0.7.8 → 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 +506 -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/dist/index.js
CHANGED
|
@@ -15,6 +15,132 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
function assertApp(app) {
|
|
22
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
23
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
24
|
+
}
|
|
25
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
26
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function envOf(options) {
|
|
30
|
+
return options.env ?? process.env;
|
|
31
|
+
}
|
|
32
|
+
function envValue(options, kind) {
|
|
33
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
34
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
function isMacOS(platform) {
|
|
37
|
+
return platform === "darwin";
|
|
38
|
+
}
|
|
39
|
+
function baseDir(kind, options) {
|
|
40
|
+
const override = envValue(options, kind);
|
|
41
|
+
if (override)
|
|
42
|
+
return override;
|
|
43
|
+
const home = options.home ?? homedir();
|
|
44
|
+
const platform = options.platform ?? process.platform;
|
|
45
|
+
if (isMacOS(platform)) {
|
|
46
|
+
switch (kind) {
|
|
47
|
+
case "config":
|
|
48
|
+
case "data":
|
|
49
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
50
|
+
case "cache":
|
|
51
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
52
|
+
case "state":
|
|
53
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
switch (kind) {
|
|
57
|
+
case "config":
|
|
58
|
+
return join(home, ".config", "hasna");
|
|
59
|
+
case "data":
|
|
60
|
+
return join(home, ".local", "share", "hasna");
|
|
61
|
+
case "state":
|
|
62
|
+
return join(home, ".local", "state", "hasna");
|
|
63
|
+
case "cache":
|
|
64
|
+
return join(home, ".cache", "hasna");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function resolvePath(kind, options) {
|
|
68
|
+
assertApp(options.app);
|
|
69
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
70
|
+
return join(baseDir(kind, options), appSegment);
|
|
71
|
+
}
|
|
72
|
+
function dataDir(options) {
|
|
73
|
+
return resolvePath("data", options);
|
|
74
|
+
}
|
|
75
|
+
var KIND_ENV, APP_SLUG_RE;
|
|
76
|
+
var init_dist = __esm(() => {
|
|
77
|
+
KIND_ENV = {
|
|
78
|
+
config: "HASNA_CONFIG_HOME",
|
|
79
|
+
data: "HASNA_DATA_HOME",
|
|
80
|
+
state: "HASNA_STATE_HOME",
|
|
81
|
+
cache: "HASNA_CACHE_HOME"
|
|
82
|
+
};
|
|
83
|
+
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/lib/app-home.ts
|
|
87
|
+
import { existsSync } from "fs";
|
|
88
|
+
import { homedir as homedir2 } from "os";
|
|
89
|
+
import { join as join2, resolve } from "path";
|
|
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
|
+
var STORE_MARKER = "hooks.db";
|
|
140
|
+
var init_app_home = __esm(() => {
|
|
141
|
+
init_dist();
|
|
142
|
+
});
|
|
143
|
+
|
|
18
144
|
// src/db/schema.ts
|
|
19
145
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
20
146
|
CREATE TABLE IF NOT EXISTS hook_events (
|
|
@@ -209,9 +335,9 @@ var init_migrations = __esm(() => {
|
|
|
209
335
|
});
|
|
210
336
|
|
|
211
337
|
// src/db/legacy-import.ts
|
|
212
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
213
|
-
import { join } from "path";
|
|
214
|
-
import { homedir } from "os";
|
|
338
|
+
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
339
|
+
import { join as join3 } from "path";
|
|
340
|
+
import { homedir as homedir3 } from "os";
|
|
215
341
|
function ensureMetaTable(db) {
|
|
216
342
|
db.exec(`
|
|
217
343
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -285,25 +411,25 @@ function importErrorsLog(db, filePath) {
|
|
|
285
411
|
} catch {}
|
|
286
412
|
return count;
|
|
287
413
|
}
|
|
288
|
-
function runLegacyImport(db, homeDir =
|
|
414
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
289
415
|
try {
|
|
290
416
|
if (isAlreadyDone(db))
|
|
291
417
|
return;
|
|
292
418
|
let total = 0;
|
|
293
|
-
const claudeProjectsDir =
|
|
294
|
-
if (
|
|
419
|
+
const claudeProjectsDir = join3(homeDir, ".claude", "projects");
|
|
420
|
+
if (existsSync2(claudeProjectsDir)) {
|
|
295
421
|
try {
|
|
296
422
|
const projectDirs = readdirSync(claudeProjectsDir);
|
|
297
423
|
for (const dir of projectDirs) {
|
|
298
|
-
const projectDir =
|
|
424
|
+
const projectDir = join3(claudeProjectsDir, dir);
|
|
299
425
|
try {
|
|
300
426
|
const files = readdirSync(projectDir);
|
|
301
427
|
for (const file of files) {
|
|
302
428
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
303
|
-
total += importJsonlFile(db,
|
|
429
|
+
total += importJsonlFile(db, join3(projectDir, file));
|
|
304
430
|
}
|
|
305
431
|
if (file === "errors.log") {
|
|
306
|
-
total += importErrorsLog(db,
|
|
432
|
+
total += importErrorsLog(db, join3(projectDir, file));
|
|
307
433
|
}
|
|
308
434
|
}
|
|
309
435
|
} catch {}
|
|
@@ -339,31 +465,27 @@ function runRetention(db, days) {
|
|
|
339
465
|
|
|
340
466
|
// src/db/index.ts
|
|
341
467
|
import { Database } from "bun:sqlite";
|
|
342
|
-
import { existsSync as
|
|
343
|
-
import { join as
|
|
344
|
-
import { homedir as homedir2 } from "os";
|
|
468
|
+
import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
|
|
469
|
+
import { join as join4 } from "path";
|
|
345
470
|
function resolveDataDir() {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!existsSync2(newDir) && existsSync2(oldDir)) {
|
|
352
|
-
mkdirSync(join2(homedir2(), ".hasna"), { recursive: true });
|
|
353
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
471
|
+
const effective = getEffectiveDataRoot();
|
|
472
|
+
const oldDir = join4(getHomeDir(), ".hooks");
|
|
473
|
+
if (!existsSync3(effective) && existsSync3(oldDir)) {
|
|
474
|
+
mkdirSync(effective, { recursive: true });
|
|
475
|
+
cpSync(oldDir, effective, { recursive: true });
|
|
354
476
|
}
|
|
355
|
-
return
|
|
477
|
+
return effective;
|
|
356
478
|
}
|
|
357
479
|
function getDbPath() {
|
|
358
480
|
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
359
481
|
if (explicitDb)
|
|
360
482
|
return explicitDb;
|
|
361
|
-
const
|
|
362
|
-
return
|
|
483
|
+
const dataDir2 = resolveDataDir();
|
|
484
|
+
return join4(dataDir2, "hooks.db");
|
|
363
485
|
}
|
|
364
486
|
function ensureDir(dbPath) {
|
|
365
487
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
366
|
-
if (dir && !
|
|
488
|
+
if (dir && !existsSync3(dir)) {
|
|
367
489
|
mkdirSync(dir, { recursive: true });
|
|
368
490
|
}
|
|
369
491
|
}
|
|
@@ -371,7 +493,7 @@ function getDb() {
|
|
|
371
493
|
if (instance)
|
|
372
494
|
return instance;
|
|
373
495
|
const dbPath = getDbPath();
|
|
374
|
-
const isNew = dbPath === ":memory:" || !
|
|
496
|
+
const isNew = dbPath === ":memory:" || !existsSync3(dbPath);
|
|
375
497
|
ensureDir(dbPath);
|
|
376
498
|
instance = new Database(dbPath);
|
|
377
499
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -395,6 +517,7 @@ function getDb() {
|
|
|
395
517
|
}
|
|
396
518
|
var instance = null;
|
|
397
519
|
var init_db = __esm(() => {
|
|
520
|
+
init_app_home();
|
|
398
521
|
init_migrations();
|
|
399
522
|
init_legacy_import();
|
|
400
523
|
});
|
|
@@ -4421,7 +4544,8 @@ var init_zod = __esm(() => {
|
|
|
4421
4544
|
|
|
4422
4545
|
// ../contracts/dist/client/transport.js
|
|
4423
4546
|
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
4424
|
-
import {
|
|
4547
|
+
import { createRequire } from "module";
|
|
4548
|
+
import { join as join5 } from "path";
|
|
4425
4549
|
function envToken(name) {
|
|
4426
4550
|
return name.toUpperCase().replace(/-/g, "_");
|
|
4427
4551
|
}
|
|
@@ -4435,24 +4559,48 @@ function clientTransportEnvKeys(name) {
|
|
|
4435
4559
|
function credentialOverrideEnvKey(name) {
|
|
4436
4560
|
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
4437
4561
|
}
|
|
4562
|
+
function credentialPointerEnvKey(name) {
|
|
4563
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
4564
|
+
}
|
|
4438
4565
|
function homeDir(env) {
|
|
4439
4566
|
const home = env.HOME?.trim();
|
|
4440
4567
|
return home ? home : null;
|
|
4441
4568
|
}
|
|
4442
|
-
function
|
|
4443
|
-
return profileDiskSources(name, env, null);
|
|
4444
|
-
}
|
|
4445
|
-
function profileDiskSources(name, env, profile) {
|
|
4569
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
4446
4570
|
const home = homeDir(env);
|
|
4447
4571
|
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4448
4572
|
return [];
|
|
4449
4573
|
const stem = profile ? `${name}.${profile}` : name;
|
|
4450
4574
|
const configStem = profile ? `${name}-${profile}` : name;
|
|
4451
4575
|
return [
|
|
4452
|
-
|
|
4453
|
-
|
|
4576
|
+
{
|
|
4577
|
+
path: join5(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4578
|
+
tier: "fleet-env",
|
|
4579
|
+
deprecated: false
|
|
4580
|
+
},
|
|
4581
|
+
{
|
|
4582
|
+
path: join5(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
|
|
4583
|
+
tier: "legacy-cloud",
|
|
4584
|
+
deprecated: true
|
|
4585
|
+
},
|
|
4586
|
+
{
|
|
4587
|
+
path: join5(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
|
|
4588
|
+
tier: "config",
|
|
4589
|
+
deprecated: false
|
|
4590
|
+
},
|
|
4591
|
+
{
|
|
4592
|
+
path: join5(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
|
|
4593
|
+
tier: "config-legacy",
|
|
4594
|
+
deprecated: true
|
|
4595
|
+
}
|
|
4454
4596
|
];
|
|
4455
4597
|
}
|
|
4598
|
+
function credentialDiskSources(name, env) {
|
|
4599
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
4600
|
+
}
|
|
4601
|
+
function profileDiskSources(name, env, profile) {
|
|
4602
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
4603
|
+
}
|
|
4456
4604
|
function parseEnvFile(text) {
|
|
4457
4605
|
const values = new Map;
|
|
4458
4606
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -4503,6 +4651,9 @@ function readCredentialFile(path, apiKeyKeys) {
|
|
|
4503
4651
|
return null;
|
|
4504
4652
|
}
|
|
4505
4653
|
function assertUsableCredential(appName, source, value) {
|
|
4654
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
4655
|
+
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]);
|
|
4656
|
+
}
|
|
4506
4657
|
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
4507
4658
|
return;
|
|
4508
4659
|
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]);
|
|
@@ -4524,6 +4675,14 @@ function sealCredential(fields) {
|
|
|
4524
4675
|
writable: false,
|
|
4525
4676
|
configurable: false
|
|
4526
4677
|
});
|
|
4678
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
4679
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
4680
|
+
value: fields.pointerVaultKey,
|
|
4681
|
+
enumerable: false,
|
|
4682
|
+
writable: false,
|
|
4683
|
+
configurable: false
|
|
4684
|
+
});
|
|
4685
|
+
}
|
|
4527
4686
|
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4528
4687
|
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4529
4688
|
enumerable: false,
|
|
@@ -4595,6 +4754,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4595
4754
|
warning: null
|
|
4596
4755
|
});
|
|
4597
4756
|
}
|
|
4757
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
4758
|
+
const pointerRaw = env[pointerKeyName];
|
|
4759
|
+
if (pointerRaw !== undefined) {
|
|
4760
|
+
const pointer = pointerRaw.trim();
|
|
4761
|
+
if (!pointer) {
|
|
4762
|
+
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]);
|
|
4763
|
+
}
|
|
4764
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
4765
|
+
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]);
|
|
4766
|
+
}
|
|
4767
|
+
return sealCredential({
|
|
4768
|
+
apiKey: "",
|
|
4769
|
+
pointerVaultKey: pointer,
|
|
4770
|
+
tier: "pointer",
|
|
4771
|
+
source: pointerKeyName,
|
|
4772
|
+
deliberate: true,
|
|
4773
|
+
deprecated: false,
|
|
4774
|
+
diskCandidates: diskPaths,
|
|
4775
|
+
warning: null
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4598
4778
|
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4599
4779
|
if (profile) {
|
|
4600
4780
|
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
@@ -4619,26 +4799,42 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4619
4799
|
}
|
|
4620
4800
|
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);
|
|
4621
4801
|
}
|
|
4622
|
-
const
|
|
4802
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
4803
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4623
4804
|
if (diskHits.length > 0) {
|
|
4624
4805
|
const winner = diskHits[0];
|
|
4625
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
4806
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
4626
4807
|
const divergentSources = [
|
|
4627
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
4808
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
4628
4809
|
...(() => {
|
|
4629
4810
|
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4630
4811
|
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4631
4812
|
})()
|
|
4632
4813
|
];
|
|
4633
|
-
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;
|
|
4814
|
+
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;
|
|
4815
|
+
let deprecated = winner.src.deprecated;
|
|
4816
|
+
let finalWarning = warning;
|
|
4817
|
+
if (winner.src.deprecated) {
|
|
4818
|
+
deprecated = true;
|
|
4819
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4820
|
+
const notified = deprecationNotified();
|
|
4821
|
+
const noticeKey = `${name}:${winner.src.path}`;
|
|
4822
|
+
if (!notified.has(noticeKey)) {
|
|
4823
|
+
notified.add(noticeKey);
|
|
4824
|
+
const target = diskSourceList[0]?.path ?? "<none>";
|
|
4825
|
+
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.`;
|
|
4826
|
+
sink(message);
|
|
4827
|
+
}
|
|
4828
|
+
finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
|
|
4829
|
+
}
|
|
4634
4830
|
return sealCredential({
|
|
4635
4831
|
apiKey: winner.value,
|
|
4636
|
-
tier:
|
|
4637
|
-
source: winner.path,
|
|
4832
|
+
tier: winner.src.tier,
|
|
4833
|
+
source: winner.src.path,
|
|
4638
4834
|
deliberate: false,
|
|
4639
|
-
deprecated
|
|
4835
|
+
deprecated,
|
|
4640
4836
|
diskCandidates: diskPaths,
|
|
4641
|
-
warning
|
|
4837
|
+
warning: finalWarning
|
|
4642
4838
|
});
|
|
4643
4839
|
}
|
|
4644
4840
|
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
@@ -4664,7 +4860,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
4664
4860
|
}
|
|
4665
4861
|
return null;
|
|
4666
4862
|
}
|
|
4667
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
4863
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "fleet-env", LEGACY_CLOUD_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
4668
4864
|
var init_transport = __esm(() => {
|
|
4669
4865
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
4670
4866
|
appName;
|
|
@@ -4680,9 +4876,12 @@ var init_transport = __esm(() => {
|
|
|
4680
4876
|
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4681
4877
|
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4682
4878
|
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4879
|
+
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
4683
4880
|
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4684
4881
|
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4685
4882
|
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4883
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
4884
|
+
requireSecretsSdk = createRequire(import.meta.url);
|
|
4686
4885
|
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4687
4886
|
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4688
4887
|
"host",
|
|
@@ -4694,34 +4893,30 @@ var init_transport = __esm(() => {
|
|
|
4694
4893
|
});
|
|
4695
4894
|
|
|
4696
4895
|
// src/config.ts
|
|
4697
|
-
import { existsSync as
|
|
4698
|
-
import { join as
|
|
4699
|
-
import { homedir as homedir3 } from "os";
|
|
4896
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
4897
|
+
import { join as join6 } from "path";
|
|
4700
4898
|
function getHooksDataDir() {
|
|
4701
|
-
|
|
4702
|
-
if (explicit)
|
|
4703
|
-
return explicit;
|
|
4704
|
-
return join4(homedir3(), ".hasna", "hooks");
|
|
4899
|
+
return getEffectiveDataRoot();
|
|
4705
4900
|
}
|
|
4706
4901
|
function getCustomHooksDir() {
|
|
4707
|
-
return
|
|
4902
|
+
return join6(getHooksDataDir(), "hooks");
|
|
4708
4903
|
}
|
|
4709
4904
|
function getLockPath() {
|
|
4710
4905
|
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4711
4906
|
if (explicit)
|
|
4712
4907
|
return explicit;
|
|
4713
|
-
return
|
|
4908
|
+
return join6(getHooksDataDir(), "hooks.lock");
|
|
4714
4909
|
}
|
|
4715
4910
|
function getConfigPath() {
|
|
4716
4911
|
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4717
4912
|
if (explicit)
|
|
4718
4913
|
return explicit;
|
|
4719
|
-
return
|
|
4914
|
+
return join6(getHooksDataDir(), "config.json");
|
|
4720
4915
|
}
|
|
4721
4916
|
function readConfig() {
|
|
4722
4917
|
try {
|
|
4723
4918
|
const path = getConfigPath();
|
|
4724
|
-
if (!
|
|
4919
|
+
if (!existsSync4(path))
|
|
4725
4920
|
return {};
|
|
4726
4921
|
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
4727
4922
|
} catch {
|
|
@@ -4730,7 +4925,7 @@ function readConfig() {
|
|
|
4730
4925
|
}
|
|
4731
4926
|
function writeConfig(config) {
|
|
4732
4927
|
const path = getConfigPath();
|
|
4733
|
-
mkdirSync2(
|
|
4928
|
+
mkdirSync2(join6(path, ".."), { recursive: true });
|
|
4734
4929
|
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4735
4930
|
`, "utf-8");
|
|
4736
4931
|
return path;
|
|
@@ -4747,6 +4942,7 @@ function resolveApiKey() {
|
|
|
4747
4942
|
}
|
|
4748
4943
|
var init_config = __esm(() => {
|
|
4749
4944
|
init_transport();
|
|
4945
|
+
init_app_home();
|
|
4750
4946
|
});
|
|
4751
4947
|
|
|
4752
4948
|
// src/lib/semver.ts
|
|
@@ -4773,8 +4969,8 @@ __export(exports_manifest, {
|
|
|
4773
4969
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
4774
4970
|
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4775
4971
|
});
|
|
4776
|
-
import { existsSync as
|
|
4777
|
-
import { basename, dirname, isAbsolute, join as
|
|
4972
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4973
|
+
import { basename, dirname, isAbsolute, join as join7, normalize, resolve as resolve2, sep } from "path";
|
|
4778
4974
|
function parseManifest(json) {
|
|
4779
4975
|
const parsed = JSON.parse(json);
|
|
4780
4976
|
return manifestSchema.parse(parsed);
|
|
@@ -4789,8 +4985,8 @@ function isWithin(child, parent) {
|
|
|
4789
4985
|
return child === parent || child.startsWith(parent + sep);
|
|
4790
4986
|
}
|
|
4791
4987
|
function assertContainedLexical(target, root, raw) {
|
|
4792
|
-
const lexical =
|
|
4793
|
-
const rootLexical =
|
|
4988
|
+
const lexical = resolve2(target);
|
|
4989
|
+
const rootLexical = resolve2(root);
|
|
4794
4990
|
if (!isWithin(lexical, rootLexical)) {
|
|
4795
4991
|
throw new ScriptContainmentError(target, root, raw);
|
|
4796
4992
|
}
|
|
@@ -4799,14 +4995,14 @@ function assertContainedLexical(target, root, raw) {
|
|
|
4799
4995
|
function realpathNearest(path) {
|
|
4800
4996
|
const tail = [];
|
|
4801
4997
|
let cur = path;
|
|
4802
|
-
while (!
|
|
4998
|
+
while (!existsSync5(cur)) {
|
|
4803
4999
|
const parent = dirname(cur);
|
|
4804
5000
|
if (parent === cur)
|
|
4805
5001
|
break;
|
|
4806
5002
|
tail.unshift(basename(cur));
|
|
4807
5003
|
cur = parent;
|
|
4808
5004
|
}
|
|
4809
|
-
return
|
|
5005
|
+
return join7(realpathSync(cur), ...tail);
|
|
4810
5006
|
}
|
|
4811
5007
|
function assertContainedReal(root, target, raw) {
|
|
4812
5008
|
const rootReal = realpathSync(root);
|
|
@@ -4839,25 +5035,25 @@ function resolveScript(manifest, manifestDir) {
|
|
|
4839
5035
|
if (isInlineScript(manifest))
|
|
4840
5036
|
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4841
5037
|
const rel = scriptRelFor(manifest);
|
|
4842
|
-
const resolved = assertContained(
|
|
4843
|
-
if (!
|
|
5038
|
+
const resolved = assertContained(join7(manifestDir, rel), manifestDir, manifest.script);
|
|
5039
|
+
if (!existsSync5(resolved)) {
|
|
4844
5040
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4845
5041
|
}
|
|
4846
5042
|
return { path: rel, content: readFileSync4(resolved, "utf-8") };
|
|
4847
5043
|
}
|
|
4848
5044
|
function customHookDir(name) {
|
|
4849
|
-
return
|
|
5045
|
+
return join7(getCustomHooksDir(), shortManifestName(name));
|
|
4850
5046
|
}
|
|
4851
5047
|
function readCustomManifest(name) {
|
|
4852
5048
|
const dir = customHookDir(name);
|
|
4853
|
-
const manifestPath =
|
|
4854
|
-
if (!
|
|
5049
|
+
const manifestPath = join7(dir, "manifest.json");
|
|
5050
|
+
if (!existsSync5(manifestPath))
|
|
4855
5051
|
return;
|
|
4856
5052
|
try {
|
|
4857
5053
|
const manifest = parseManifest(readFileSync4(manifestPath, "utf-8"));
|
|
4858
5054
|
const script = resolveScript(manifest, dir);
|
|
4859
5055
|
const scriptIsInline = isInlineScript(manifest);
|
|
4860
|
-
return { manifest, scriptPath:
|
|
5056
|
+
return { manifest, scriptPath: join7(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4861
5057
|
} catch (err) {
|
|
4862
5058
|
if (err instanceof ScriptContainmentError)
|
|
4863
5059
|
throw err;
|
|
@@ -4866,7 +5062,7 @@ function readCustomManifest(name) {
|
|
|
4866
5062
|
}
|
|
4867
5063
|
function listCustomHooks() {
|
|
4868
5064
|
const dir = getCustomHooksDir();
|
|
4869
|
-
if (!
|
|
5065
|
+
if (!existsSync5(dir))
|
|
4870
5066
|
return [];
|
|
4871
5067
|
const results = [];
|
|
4872
5068
|
for (const entry of readdirSafe(dir)) {
|
|
@@ -4881,7 +5077,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4881
5077
|
if (isAbsolute(scriptRelPath)) {
|
|
4882
5078
|
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4883
5079
|
}
|
|
4884
|
-
const scriptPath = scriptRelPath.includes("/") ?
|
|
5080
|
+
const scriptPath = scriptRelPath.includes("/") ? join7(dir, scriptRelPath) : join7(dir, scriptRelPath);
|
|
4885
5081
|
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4886
5082
|
mkdirSync3(dir, { recursive: true });
|
|
4887
5083
|
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
@@ -4889,7 +5085,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4889
5085
|
if (scriptDir !== dir)
|
|
4890
5086
|
mkdirSync3(scriptDir, { recursive: true });
|
|
4891
5087
|
writeFileSync2(scriptPath, scriptContent, "utf-8");
|
|
4892
|
-
writeFileSync2(
|
|
5088
|
+
writeFileSync2(join7(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4893
5089
|
`, "utf-8");
|
|
4894
5090
|
return { dir, scriptPath };
|
|
4895
5091
|
}
|
|
@@ -4947,7 +5143,7 @@ __export(exports_store, {
|
|
|
4947
5143
|
LockFileError: () => LockFileError
|
|
4948
5144
|
});
|
|
4949
5145
|
import { createHash } from "crypto";
|
|
4950
|
-
import { existsSync as
|
|
5146
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4951
5147
|
import { readFile } from "fs/promises";
|
|
4952
5148
|
import { randomBytes } from "crypto";
|
|
4953
5149
|
function sha256Of(input) {
|
|
@@ -5009,7 +5205,7 @@ function assertLockShape(parsed, path) {
|
|
|
5009
5205
|
}
|
|
5010
5206
|
function readLock() {
|
|
5011
5207
|
const path = getLockPath();
|
|
5012
|
-
if (!
|
|
5208
|
+
if (!existsSync7(path))
|
|
5013
5209
|
return { hooks: {} };
|
|
5014
5210
|
let raw;
|
|
5015
5211
|
try {
|
|
@@ -5147,25 +5343,24 @@ __export(exports_profiles, {
|
|
|
5147
5343
|
deleteProfile: () => deleteProfile,
|
|
5148
5344
|
createProfile: () => createProfile
|
|
5149
5345
|
});
|
|
5150
|
-
import { existsSync as
|
|
5151
|
-
import { join as
|
|
5152
|
-
import { homedir as homedir5 } from "os";
|
|
5346
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
5347
|
+
import { join as join10 } from "path";
|
|
5153
5348
|
function resolveProfilesDir() {
|
|
5154
|
-
const newDir =
|
|
5155
|
-
const oldDir =
|
|
5156
|
-
if (!
|
|
5157
|
-
mkdirSync6(
|
|
5349
|
+
const newDir = join10(getEffectiveDataRoot(), "profiles");
|
|
5350
|
+
const oldDir = join10(getHomeDir(), ".hooks", "profiles");
|
|
5351
|
+
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
5352
|
+
mkdirSync6(newDir, { recursive: true });
|
|
5158
5353
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5159
5354
|
}
|
|
5160
5355
|
return newDir;
|
|
5161
5356
|
}
|
|
5162
5357
|
function ensureProfilesDir() {
|
|
5163
|
-
if (!
|
|
5358
|
+
if (!existsSync9(PROFILES_DIR)) {
|
|
5164
5359
|
mkdirSync6(PROFILES_DIR, { recursive: true });
|
|
5165
5360
|
}
|
|
5166
5361
|
}
|
|
5167
5362
|
function profilePath(id) {
|
|
5168
|
-
return
|
|
5363
|
+
return join10(PROFILES_DIR, `${id}.json`);
|
|
5169
5364
|
}
|
|
5170
5365
|
function shortUuid() {
|
|
5171
5366
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -5194,7 +5389,7 @@ function createProfile(input) {
|
|
|
5194
5389
|
function getProfile(id) {
|
|
5195
5390
|
const path = profilePath(id);
|
|
5196
5391
|
try {
|
|
5197
|
-
if (!
|
|
5392
|
+
if (!existsSync9(path))
|
|
5198
5393
|
return null;
|
|
5199
5394
|
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
5200
5395
|
} catch {
|
|
@@ -5202,14 +5397,14 @@ function getProfile(id) {
|
|
|
5202
5397
|
}
|
|
5203
5398
|
}
|
|
5204
5399
|
function listProfiles() {
|
|
5205
|
-
if (!
|
|
5400
|
+
if (!existsSync9(PROFILES_DIR))
|
|
5206
5401
|
return [];
|
|
5207
5402
|
try {
|
|
5208
5403
|
const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
5209
5404
|
const profiles = [];
|
|
5210
5405
|
for (const file of files) {
|
|
5211
5406
|
try {
|
|
5212
|
-
const content = readFileSync7(
|
|
5407
|
+
const content = readFileSync7(join10(PROFILES_DIR, file), "utf-8");
|
|
5213
5408
|
profiles.push(JSON.parse(content));
|
|
5214
5409
|
} catch {}
|
|
5215
5410
|
}
|
|
@@ -5232,7 +5427,7 @@ function updateProfile(id, data) {
|
|
|
5232
5427
|
}
|
|
5233
5428
|
function deleteProfile(id) {
|
|
5234
5429
|
const path = profilePath(id);
|
|
5235
|
-
if (!
|
|
5430
|
+
if (!existsSync9(path))
|
|
5236
5431
|
return false;
|
|
5237
5432
|
try {
|
|
5238
5433
|
rmSync2(path);
|
|
@@ -5262,7 +5457,7 @@ function importProfiles(profiles) {
|
|
|
5262
5457
|
continue;
|
|
5263
5458
|
}
|
|
5264
5459
|
const path = profilePath(profile.agent_id);
|
|
5265
|
-
if (
|
|
5460
|
+
if (existsSync9(path)) {
|
|
5266
5461
|
skipped++;
|
|
5267
5462
|
continue;
|
|
5268
5463
|
}
|
|
@@ -5274,12 +5469,13 @@ function importProfiles(profiles) {
|
|
|
5274
5469
|
}
|
|
5275
5470
|
var PROFILES_DIR;
|
|
5276
5471
|
var init_profiles = __esm(() => {
|
|
5472
|
+
init_app_home();
|
|
5277
5473
|
PROFILES_DIR = resolveProfilesDir();
|
|
5278
5474
|
});
|
|
5279
5475
|
|
|
5280
5476
|
// src/lib/hook-env.ts
|
|
5281
5477
|
import { dirname as dirname5, isAbsolute as isAbsolute2 } from "path";
|
|
5282
|
-
import { existsSync as
|
|
5478
|
+
import { existsSync as existsSync11, statSync as statSync2 } from "fs";
|
|
5283
5479
|
function isDeniedEnvName(name) {
|
|
5284
5480
|
const upper = name.toUpperCase();
|
|
5285
5481
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -5313,7 +5509,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
5313
5509
|
}
|
|
5314
5510
|
function sanitizeHookPath(pathValue, home) {
|
|
5315
5511
|
const baseline = PATH_BASELINE.slice();
|
|
5316
|
-
if (process.platform === "darwin" &&
|
|
5512
|
+
if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
|
|
5317
5513
|
baseline.push("/opt/homebrew/bin");
|
|
5318
5514
|
const bunDir = dirname5(process.execPath);
|
|
5319
5515
|
const seen = new Set;
|
|
@@ -5439,7 +5635,7 @@ __export(exports_run, {
|
|
|
5439
5635
|
});
|
|
5440
5636
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5441
5637
|
import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
5442
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
5638
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join13 } from "path";
|
|
5443
5639
|
function extensionOf(scriptPath) {
|
|
5444
5640
|
const base = basename2(scriptPath);
|
|
5445
5641
|
const dot = base.lastIndexOf(".");
|
|
@@ -5510,7 +5706,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
5510
5706
|
async function executeVerifiedScript(options) {
|
|
5511
5707
|
const scriptDir = dirname6(options.scriptPath);
|
|
5512
5708
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5513
|
-
const tempPath =
|
|
5709
|
+
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5514
5710
|
let proc = null;
|
|
5515
5711
|
let timedOut = false;
|
|
5516
5712
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6125,6 +6321,7 @@ function coerceForSqlite(value) {
|
|
|
6125
6321
|
return String(value);
|
|
6126
6322
|
}
|
|
6127
6323
|
// src/lib/registry.ts
|
|
6324
|
+
init_app_home();
|
|
6128
6325
|
var HOOK_EVENTS = [
|
|
6129
6326
|
"PreToolUse",
|
|
6130
6327
|
"PostToolUse",
|
|
@@ -6462,7 +6659,7 @@ var HOOKS = [
|
|
|
6462
6659
|
{
|
|
6463
6660
|
name: "sessionlog",
|
|
6464
6661
|
displayName: "Session Log",
|
|
6465
|
-
description:
|
|
6662
|
+
description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
|
|
6466
6663
|
version: "0.1.0",
|
|
6467
6664
|
category: "Observability",
|
|
6468
6665
|
event: "PostToolUse",
|
|
@@ -6472,7 +6669,7 @@ var HOOKS = [
|
|
|
6472
6669
|
{
|
|
6473
6670
|
name: "commandlog",
|
|
6474
6671
|
displayName: "Command Log",
|
|
6475
|
-
description:
|
|
6672
|
+
description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
|
|
6476
6673
|
version: "0.1.0",
|
|
6477
6674
|
category: "Observability",
|
|
6478
6675
|
event: "PostToolUse",
|
|
@@ -6492,7 +6689,7 @@ var HOOKS = [
|
|
|
6492
6689
|
{
|
|
6493
6690
|
name: "errornotify",
|
|
6494
6691
|
displayName: "Error Notify",
|
|
6495
|
-
description:
|
|
6692
|
+
description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
|
|
6496
6693
|
version: "0.1.0",
|
|
6497
6694
|
category: "Observability",
|
|
6498
6695
|
event: "PostToolUse",
|
|
@@ -6674,21 +6871,21 @@ function getHook(name) {
|
|
|
6674
6871
|
return HOOKS.find((h) => h.name === name);
|
|
6675
6872
|
}
|
|
6676
6873
|
// src/lib/installer.ts
|
|
6677
|
-
import { existsSync as
|
|
6678
|
-
import { join as
|
|
6874
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
6875
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
6679
6876
|
import { homedir as homedir4 } from "os";
|
|
6680
6877
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6681
6878
|
|
|
6682
6879
|
// src/lib/resolve.ts
|
|
6683
|
-
import { dirname as dirname2, join as
|
|
6684
|
-
import { existsSync as
|
|
6880
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
6881
|
+
import { existsSync as existsSync6 } from "fs";
|
|
6685
6882
|
import { fileURLToPath } from "url";
|
|
6686
6883
|
init_manifest();
|
|
6687
6884
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
6688
|
-
const sourceLayout =
|
|
6689
|
-
if (
|
|
6885
|
+
const sourceLayout = join8(moduleDir, "..", "..", "hooks");
|
|
6886
|
+
if (existsSync6(join8(sourceLayout, "hook-gitguard")))
|
|
6690
6887
|
return sourceLayout;
|
|
6691
|
-
return
|
|
6888
|
+
return join8(moduleDir, "..", "hooks");
|
|
6692
6889
|
}
|
|
6693
6890
|
function resolveHookDir(name) {
|
|
6694
6891
|
const custom2 = readCustomManifest(name);
|
|
@@ -6699,11 +6896,11 @@ function resolveHookDir(name) {
|
|
|
6699
6896
|
return;
|
|
6700
6897
|
const short = shortManifestName(name);
|
|
6701
6898
|
const bundledBase = resolveBundledHooksDir();
|
|
6702
|
-
const direct =
|
|
6703
|
-
if (
|
|
6899
|
+
const direct = join8(bundledBase, short);
|
|
6900
|
+
if (existsSync6(direct))
|
|
6704
6901
|
return direct;
|
|
6705
|
-
const prefixed =
|
|
6706
|
-
return
|
|
6902
|
+
const prefixed = join8(bundledBase, `hook-${short}`);
|
|
6903
|
+
return existsSync6(prefixed) ? prefixed : undefined;
|
|
6707
6904
|
}
|
|
6708
6905
|
function resolveScriptPath(name) {
|
|
6709
6906
|
const custom2 = readCustomManifest(name);
|
|
@@ -6712,8 +6909,8 @@ function resolveScriptPath(name) {
|
|
|
6712
6909
|
const dir = resolveHookDir(name);
|
|
6713
6910
|
if (!dir)
|
|
6714
6911
|
return;
|
|
6715
|
-
const script =
|
|
6716
|
-
return
|
|
6912
|
+
const script = join8(dir, "src", "hook.ts");
|
|
6913
|
+
return existsSync6(script) ? script : undefined;
|
|
6717
6914
|
}
|
|
6718
6915
|
function resolveHookMeta(name) {
|
|
6719
6916
|
const custom2 = readCustomManifest(name);
|
|
@@ -6768,7 +6965,7 @@ function resolveHook(name) {
|
|
|
6768
6965
|
init_manifest();
|
|
6769
6966
|
init_store();
|
|
6770
6967
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
6771
|
-
var HOOKS_DIR =
|
|
6968
|
+
var HOOKS_DIR = existsSync8(join9(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join9(__dirname2, "..", "..", "hooks") : join9(__dirname2, "..", "hooks");
|
|
6772
6969
|
function normalizeHookName(name) {
|
|
6773
6970
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
6774
6971
|
}
|
|
@@ -6838,24 +7035,24 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
6838
7035
|
return globalOverride;
|
|
6839
7036
|
const dir = getTargetSettingsDir(target);
|
|
6840
7037
|
if (scope === "project") {
|
|
6841
|
-
return target === "codewith" ?
|
|
7038
|
+
return target === "codewith" ? join9(process.cwd(), dir, "config.toml") : join9(process.cwd(), dir, "settings.json");
|
|
6842
7039
|
}
|
|
6843
|
-
return target === "codewith" ?
|
|
7040
|
+
return target === "codewith" ? join9(homedir4(), dir, "config.toml") : join9(homedir4(), dir, "settings.json");
|
|
6844
7041
|
}
|
|
6845
7042
|
function getHookPath(name) {
|
|
6846
7043
|
const shortName = shortHookName(name);
|
|
6847
7044
|
const resolved = resolveHookDir(shortName);
|
|
6848
7045
|
if (resolved)
|
|
6849
7046
|
return resolved;
|
|
6850
|
-
return
|
|
7047
|
+
return join9(HOOKS_DIR, normalizeHookName(shortName));
|
|
6851
7048
|
}
|
|
6852
7049
|
function hookExists(name) {
|
|
6853
|
-
return
|
|
7050
|
+
return existsSync8(getHookPath(name));
|
|
6854
7051
|
}
|
|
6855
7052
|
function readSettings(scope = "global", target = "claude") {
|
|
6856
7053
|
const path = getSettingsPath(scope, target);
|
|
6857
7054
|
try {
|
|
6858
|
-
if (
|
|
7055
|
+
if (existsSync8(path)) {
|
|
6859
7056
|
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
6860
7057
|
}
|
|
6861
7058
|
} catch (error) {
|
|
@@ -6866,7 +7063,7 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
6866
7063
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
6867
7064
|
const path = getSettingsPath(scope, target);
|
|
6868
7065
|
const dir = dirname3(path);
|
|
6869
|
-
if (!
|
|
7066
|
+
if (!existsSync8(dir)) {
|
|
6870
7067
|
mkdirSync5(dir, { recursive: true });
|
|
6871
7068
|
}
|
|
6872
7069
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -6955,7 +7152,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
6955
7152
|
function readCodewithConfig(scope, configPath) {
|
|
6956
7153
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6957
7154
|
try {
|
|
6958
|
-
return
|
|
7155
|
+
return existsSync8(path) ? readFileSync6(path, "utf-8") : "";
|
|
6959
7156
|
} catch {
|
|
6960
7157
|
return "";
|
|
6961
7158
|
}
|
|
@@ -6963,9 +7160,9 @@ function readCodewithConfig(scope, configPath) {
|
|
|
6963
7160
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
6964
7161
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6965
7162
|
const dir = dirname3(path);
|
|
6966
|
-
if (!
|
|
7163
|
+
if (!existsSync8(dir))
|
|
6967
7164
|
mkdirSync5(dir, { recursive: true });
|
|
6968
|
-
const existing =
|
|
7165
|
+
const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
|
|
6969
7166
|
const sep2 = existing.trim() ? `
|
|
6970
7167
|
|
|
6971
7168
|
` : "";
|
|
@@ -7266,7 +7463,7 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
7266
7463
|
}
|
|
7267
7464
|
function codewithHasHookEntry(name, scope = "global") {
|
|
7268
7465
|
const path = getSettingsPath(scope, "codewith");
|
|
7269
|
-
if (!
|
|
7466
|
+
if (!existsSync8(path))
|
|
7270
7467
|
return false;
|
|
7271
7468
|
try {
|
|
7272
7469
|
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
@@ -7300,7 +7497,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7300
7497
|
const registrationsRemaining = [];
|
|
7301
7498
|
if (target === "codewith" || target === "all") {
|
|
7302
7499
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
7303
|
-
if (
|
|
7500
|
+
if (existsSync8(codewithPath)) {
|
|
7304
7501
|
const before = readFileSync6(codewithPath, "utf-8");
|
|
7305
7502
|
const after = removeCodewithHookEntry(before, shortName);
|
|
7306
7503
|
if (after.removed) {
|
|
@@ -7345,17 +7542,17 @@ init_profiles();
|
|
|
7345
7542
|
init_config();
|
|
7346
7543
|
init_store();
|
|
7347
7544
|
init_manifest();
|
|
7348
|
-
import { existsSync as
|
|
7545
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
7349
7546
|
|
|
7350
7547
|
// src/lib/custom-install.ts
|
|
7351
7548
|
init_config();
|
|
7352
7549
|
init_manifest();
|
|
7353
|
-
import { existsSync as
|
|
7354
|
-
import { join as
|
|
7550
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
|
|
7551
|
+
import { join as join11 } from "path";
|
|
7355
7552
|
import { tmpdir } from "os";
|
|
7356
7553
|
function manifestAt(dir) {
|
|
7357
|
-
const path =
|
|
7358
|
-
if (!
|
|
7554
|
+
const path = join11(dir, "manifest.json");
|
|
7555
|
+
if (!existsSync10(path))
|
|
7359
7556
|
return;
|
|
7360
7557
|
try {
|
|
7361
7558
|
return parseManifest(readFileSync8(path, "utf-8"));
|
|
@@ -7377,8 +7574,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7377
7574
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7378
7575
|
}
|
|
7379
7576
|
async function cloneToTemp(gitUrl) {
|
|
7380
|
-
const tmp = mkdtempSync(
|
|
7381
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
7577
|
+
const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
|
|
7578
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
|
|
7382
7579
|
stdout: "pipe",
|
|
7383
7580
|
stderr: "pipe"
|
|
7384
7581
|
});
|
|
@@ -7391,7 +7588,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
7391
7588
|
rmSync3(tmp, { recursive: true, force: true });
|
|
7392
7589
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7393
7590
|
}
|
|
7394
|
-
return
|
|
7591
|
+
return join11(tmp, "repo");
|
|
7395
7592
|
}
|
|
7396
7593
|
function isGitUrl(value) {
|
|
7397
7594
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7401,7 +7598,7 @@ function isCustomSource(value) {
|
|
|
7401
7598
|
return true;
|
|
7402
7599
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7403
7600
|
return true;
|
|
7404
|
-
return
|
|
7601
|
+
return existsSync10(value);
|
|
7405
7602
|
}
|
|
7406
7603
|
async function installCustomSource(source) {
|
|
7407
7604
|
if (isGitUrl(source)) {
|
|
@@ -7415,7 +7612,7 @@ async function installCustomSource(source) {
|
|
|
7415
7612
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7416
7613
|
return installManifestUrl(source);
|
|
7417
7614
|
}
|
|
7418
|
-
if (!
|
|
7615
|
+
if (!existsSync10(source)) {
|
|
7419
7616
|
throw new Error(`No such local path: ${source}`);
|
|
7420
7617
|
}
|
|
7421
7618
|
if (source.endsWith("manifest.json")) {
|
|
@@ -7715,7 +7912,7 @@ init_manifest();
|
|
|
7715
7912
|
init_store();
|
|
7716
7913
|
init_config();
|
|
7717
7914
|
import { readFileSync as readFileSync10 } from "fs";
|
|
7718
|
-
import { dirname as dirname4, join as
|
|
7915
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
7719
7916
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7720
7917
|
|
|
7721
7918
|
// src/lib/secure-compare.ts
|
|
@@ -7889,7 +8086,7 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
7889
8086
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
7890
8087
|
function packageVersion() {
|
|
7891
8088
|
try {
|
|
7892
|
-
const pkgPath =
|
|
8089
|
+
const pkgPath = join12(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
7893
8090
|
return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
|
|
7894
8091
|
} catch {
|
|
7895
8092
|
return "0.0.0";
|
|
@@ -8038,9 +8235,15 @@ function handleServeRequest(req, apiKey) {
|
|
|
8038
8235
|
}
|
|
8039
8236
|
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
8040
8237
|
}
|
|
8238
|
+
function resolveServeOptions(options) {
|
|
8239
|
+
const envPort = process.env.PORT?.trim();
|
|
8240
|
+
const envHost = process.env.HOST?.trim();
|
|
8241
|
+
const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
|
|
8242
|
+
const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
|
|
8243
|
+
return { port, host };
|
|
8244
|
+
}
|
|
8041
8245
|
function startServeServer(options) {
|
|
8042
|
-
const port = options
|
|
8043
|
-
const host = options.host ?? SERVE_HOST;
|
|
8246
|
+
const { port, host } = resolveServeOptions(options);
|
|
8044
8247
|
const apiKey = resolveApiKey();
|
|
8045
8248
|
const server = Bun.serve({
|
|
8046
8249
|
hostname: host,
|
|
@@ -8151,7 +8354,7 @@ async function runHook(name, input, options = {}) {
|
|
|
8151
8354
|
throw new Error(`Hook '${name}' not found`);
|
|
8152
8355
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
8153
8356
|
const resolvedScript = custom2?.scriptPath;
|
|
8154
|
-
const script = resolvedScript ?? (
|
|
8357
|
+
const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
|
|
8155
8358
|
if (!script)
|
|
8156
8359
|
throw new Error(`Hook script not found: ${name}`);
|
|
8157
8360
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|