@hasna/hooks 0.7.10 → 0.8.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 +32 -6
- package/bin/index.js +1872 -1300
- package/bin/serve.js +730 -551
- package/dist/config.d.ts +25 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1198 -712
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/installer.d.ts +0 -9
- package/dist/lib/local-opt-in.d.ts +80 -0
- package/dist/lib/profiles.d.ts +10 -0
- package/dist/lib/resolver-types.d.ts +45 -0
- package/dist/lib/sync.d.ts +13 -8
- package/dist/lib/transport.d.ts +87 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/storage.js +54 -63
- package/hooks/hook-affected-tests/package.json +1 -1
- package/hooks/hook-agent-rules-version-check/package.json +1 -1
- package/hooks/hook-agentmessages/package.json +1 -1
- package/hooks/hook-announce-start/package.json +1 -1
- package/hooks/hook-announce-stop/package.json +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-branchprotect/package.json +1 -1
- package/hooks/hook-checkbugs/package.json +1 -1
- package/hooks/hook-checkdocs/package.json +1 -1
- package/hooks/hook-checkfiles/package.json +1 -1
- package/hooks/hook-checklint/package.json +1 -1
- package/hooks/hook-checkpoint/package.json +1 -1
- package/hooks/hook-checksecurity/package.json +1 -1
- package/hooks/hook-checktasks/package.json +1 -1
- package/hooks/hook-checktests/package.json +1 -1
- package/hooks/hook-conflict-detect/package.json +1 -1
- package/hooks/hook-contextrefresh/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-dm-inject/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-failure-to-task/package.json +1 -1
- package/hooks/hook-filelock/package.json +1 -1
- package/hooks/hook-fleet-blockers-gate/package.json +1 -1
- package/hooks/hook-fleet-catchup/package.json +1 -1
- package/hooks/hook-gitguard/package.json +1 -1
- package/hooks/hook-packageage/package.json +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-phonenotify/package.json +1 -1
- package/hooks/hook-precompact/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-stylescheck/package.json +1 -1
- package/hooks/hook-typecheck-gate/package.json +1 -1
- package/package.json +4 -9
- package/scripts/ensure-profiles-dir.mjs +52 -1
package/dist/index.js
CHANGED
|
@@ -15,92 +15,77 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// src/lib/app-home.ts
|
|
19
|
+
import { existsSync } from "fs";
|
|
19
20
|
import { homedir } from "os";
|
|
20
|
-
import { join } from "path";
|
|
21
|
-
|
|
21
|
+
import { join, resolve } from "path";
|
|
22
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
23
|
+
import { join as pathsResolverJoin } from "path";
|
|
24
|
+
function pathsResolverAssertApp(app) {
|
|
22
25
|
if (typeof app !== "string" || app.length === 0) {
|
|
23
26
|
throw new TypeError("paths: app must be a non-empty string");
|
|
24
27
|
}
|
|
25
|
-
if (!
|
|
28
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
26
29
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
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";
|
|
32
|
+
function pathsResolverAssertKind(kind) {
|
|
33
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
34
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
35
|
+
}
|
|
38
36
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
function pathsResolverBaseDir(kind, options) {
|
|
38
|
+
pathsResolverAssertKind(kind);
|
|
39
|
+
const env = options.env ?? process.env;
|
|
40
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
41
|
+
if (typeof override === "string" && override.length > 0)
|
|
42
42
|
return override;
|
|
43
|
-
const home = options.home ??
|
|
43
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
44
44
|
const platform = options.platform ?? process.platform;
|
|
45
|
-
if (
|
|
45
|
+
if (platform === "darwin") {
|
|
46
46
|
switch (kind) {
|
|
47
47
|
case "config":
|
|
48
48
|
case "data":
|
|
49
|
-
return
|
|
49
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
50
50
|
case "cache":
|
|
51
|
-
return
|
|
51
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
52
52
|
case "state":
|
|
53
|
-
return
|
|
53
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
switch (kind) {
|
|
57
57
|
case "config":
|
|
58
|
-
return
|
|
58
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
59
59
|
case "data":
|
|
60
|
-
return
|
|
60
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
61
61
|
case "state":
|
|
62
|
-
return
|
|
62
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
63
63
|
case "cache":
|
|
64
|
-
return
|
|
64
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
const appSegment = options.internal === true ?
|
|
70
|
-
return
|
|
67
|
+
function pathsResolverResolve(kind, options) {
|
|
68
|
+
pathsResolverAssertApp(options.app);
|
|
69
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
70
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
71
71
|
}
|
|
72
72
|
function dataDir(options) {
|
|
73
|
-
return
|
|
73
|
+
return pathsResolverResolve("data", options);
|
|
74
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
75
|
function getHomeDir(env = process.env) {
|
|
91
|
-
return env.HOME || env.USERPROFILE ||
|
|
76
|
+
return env.HOME || env.USERPROFILE || homedir();
|
|
92
77
|
}
|
|
93
78
|
function getResolverDataRoot(env = process.env) {
|
|
94
79
|
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
95
80
|
}
|
|
96
81
|
function getLegacyDataRoot(env = process.env) {
|
|
97
|
-
return
|
|
82
|
+
return join(getHomeDir(env), ".hasna", "hooks");
|
|
98
83
|
}
|
|
99
84
|
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
100
85
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
101
86
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
102
87
|
return true;
|
|
103
|
-
return existsSync(
|
|
88
|
+
return existsSync(join(resolved, STORE_MARKER));
|
|
104
89
|
}
|
|
105
90
|
function getExplicitDataDir(env = process.env) {
|
|
106
91
|
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
@@ -128,7 +113,7 @@ function getReportedDbPath(env = process.env) {
|
|
|
128
113
|
const explicit = getExplicitDbPath(env);
|
|
129
114
|
if (explicit)
|
|
130
115
|
return explicit;
|
|
131
|
-
return
|
|
116
|
+
return join(getEffectiveDataRoot(env), "hooks.db");
|
|
132
117
|
}
|
|
133
118
|
function getExplicitDbPath(env = process.env) {
|
|
134
119
|
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
@@ -136,9 +121,15 @@ function getExplicitDbPath(env = process.env) {
|
|
|
136
121
|
return db.trim();
|
|
137
122
|
return;
|
|
138
123
|
}
|
|
139
|
-
var STORE_MARKER = "hooks.db";
|
|
124
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, STORE_MARKER = "hooks.db";
|
|
140
125
|
var init_app_home = __esm(() => {
|
|
141
|
-
|
|
126
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
127
|
+
config: "HASNA_CONFIG_HOME",
|
|
128
|
+
data: "HASNA_DATA_HOME",
|
|
129
|
+
state: "HASNA_STATE_HOME",
|
|
130
|
+
cache: "HASNA_CACHE_HOME"
|
|
131
|
+
};
|
|
132
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
142
133
|
});
|
|
143
134
|
|
|
144
135
|
// src/db/schema.ts
|
|
@@ -336,8 +327,8 @@ var init_migrations = __esm(() => {
|
|
|
336
327
|
|
|
337
328
|
// src/db/legacy-import.ts
|
|
338
329
|
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
339
|
-
import { join as
|
|
340
|
-
import { homedir as
|
|
330
|
+
import { join as join2 } from "path";
|
|
331
|
+
import { homedir as homedir2 } from "os";
|
|
341
332
|
function ensureMetaTable(db) {
|
|
342
333
|
db.exec(`
|
|
343
334
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -411,25 +402,25 @@ function importErrorsLog(db, filePath) {
|
|
|
411
402
|
} catch {}
|
|
412
403
|
return count;
|
|
413
404
|
}
|
|
414
|
-
function runLegacyImport(db, homeDir =
|
|
405
|
+
function runLegacyImport(db, homeDir = homedir2()) {
|
|
415
406
|
try {
|
|
416
407
|
if (isAlreadyDone(db))
|
|
417
408
|
return;
|
|
418
409
|
let total = 0;
|
|
419
|
-
const claudeProjectsDir =
|
|
410
|
+
const claudeProjectsDir = join2(homeDir, ".claude", "projects");
|
|
420
411
|
if (existsSync2(claudeProjectsDir)) {
|
|
421
412
|
try {
|
|
422
413
|
const projectDirs = readdirSync(claudeProjectsDir);
|
|
423
414
|
for (const dir of projectDirs) {
|
|
424
|
-
const projectDir =
|
|
415
|
+
const projectDir = join2(claudeProjectsDir, dir);
|
|
425
416
|
try {
|
|
426
417
|
const files = readdirSync(projectDir);
|
|
427
418
|
for (const file of files) {
|
|
428
419
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
429
|
-
total += importJsonlFile(db,
|
|
420
|
+
total += importJsonlFile(db, join2(projectDir, file));
|
|
430
421
|
}
|
|
431
422
|
if (file === "errors.log") {
|
|
432
|
-
total += importErrorsLog(db,
|
|
423
|
+
total += importErrorsLog(db, join2(projectDir, file));
|
|
433
424
|
}
|
|
434
425
|
}
|
|
435
426
|
} catch {}
|
|
@@ -466,10 +457,10 @@ function runRetention(db, days) {
|
|
|
466
457
|
// src/db/index.ts
|
|
467
458
|
import { Database } from "bun:sqlite";
|
|
468
459
|
import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
|
|
469
|
-
import { join as
|
|
460
|
+
import { join as join3 } from "path";
|
|
470
461
|
function resolveDataDir() {
|
|
471
462
|
const effective = getEffectiveDataRoot();
|
|
472
|
-
const oldDir =
|
|
463
|
+
const oldDir = join3(getHomeDir(), ".hooks");
|
|
473
464
|
if (!existsSync3(effective) && existsSync3(oldDir)) {
|
|
474
465
|
mkdirSync(effective, { recursive: true });
|
|
475
466
|
cpSync(oldDir, effective, { recursive: true });
|
|
@@ -481,7 +472,7 @@ function getDbPath() {
|
|
|
481
472
|
if (explicitDb)
|
|
482
473
|
return explicitDb;
|
|
483
474
|
const dataDir2 = resolveDataDir();
|
|
484
|
-
return
|
|
475
|
+
return join3(dataDir2, "hooks.db");
|
|
485
476
|
}
|
|
486
477
|
function ensureDir(dbPath) {
|
|
487
478
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
@@ -568,8 +559,8 @@ var init_redact = __esm(() => {
|
|
|
568
559
|
/AKIA[0-9A-Z]{16}/g,
|
|
569
560
|
/ASIA[0-9A-Z]{16}/g,
|
|
570
561
|
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
571
|
-
/-----BEGIN [A-Z0-9 ]*
|
|
572
|
-
/-----BEGIN OPENSSH
|
|
562
|
+
/-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
|
|
563
|
+
/-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
|
|
573
564
|
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
574
565
|
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
575
566
|
/\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
|
|
@@ -4542,550 +4533,165 @@ var init_zod = __esm(() => {
|
|
|
4542
4533
|
init_external();
|
|
4543
4534
|
});
|
|
4544
4535
|
|
|
4545
|
-
//
|
|
4546
|
-
import {
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
function envToken(name) {
|
|
4550
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
4551
|
-
}
|
|
4552
|
-
function clientTransportEnvKeys(name) {
|
|
4553
|
-
const envSegment = envToken(name);
|
|
4554
|
-
return {
|
|
4555
|
-
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4556
|
-
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4557
|
-
};
|
|
4536
|
+
// src/config.ts
|
|
4537
|
+
import { join as join4 } from "path";
|
|
4538
|
+
function getHooksDataDir() {
|
|
4539
|
+
return getEffectiveDataRoot();
|
|
4558
4540
|
}
|
|
4559
|
-
function
|
|
4560
|
-
return
|
|
4541
|
+
function getCustomHooksDir() {
|
|
4542
|
+
return join4(getHooksDataDir(), "hooks");
|
|
4561
4543
|
}
|
|
4562
|
-
function
|
|
4563
|
-
|
|
4544
|
+
function getLockPath() {
|
|
4545
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4546
|
+
if (explicit)
|
|
4547
|
+
return explicit;
|
|
4548
|
+
return join4(getHooksDataDir(), "hooks.lock");
|
|
4564
4549
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4550
|
+
var init_config = __esm(() => {
|
|
4551
|
+
init_app_home();
|
|
4552
|
+
});
|
|
4553
|
+
|
|
4554
|
+
// src/lib/semver.ts
|
|
4555
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4556
|
+
var init_semver = __esm(() => {
|
|
4557
|
+
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4558
|
+
});
|
|
4559
|
+
|
|
4560
|
+
// src/lib/manifest.ts
|
|
4561
|
+
var exports_manifest = {};
|
|
4562
|
+
__export(exports_manifest, {
|
|
4563
|
+
writeCustomHook: () => writeCustomHook,
|
|
4564
|
+
shortManifestName: () => shortManifestName,
|
|
4565
|
+
scriptRelFor: () => scriptRelFor,
|
|
4566
|
+
resolveScript: () => resolveScript,
|
|
4567
|
+
readCustomManifest: () => readCustomManifest,
|
|
4568
|
+
parseManifest: () => parseManifest,
|
|
4569
|
+
normalizeManifestName: () => normalizeManifestName,
|
|
4570
|
+
manifestSchema: () => manifestSchema,
|
|
4571
|
+
listCustomHooks: () => listCustomHooks,
|
|
4572
|
+
isInlineScript: () => isInlineScript,
|
|
4573
|
+
customHookDir: () => customHookDir,
|
|
4574
|
+
assertContained: () => assertContained,
|
|
4575
|
+
ScriptContainmentError: () => ScriptContainmentError,
|
|
4576
|
+
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4577
|
+
});
|
|
4578
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync, writeFileSync } from "fs";
|
|
4579
|
+
import { basename, dirname, isAbsolute, join as join5, normalize, resolve as resolve2, sep } from "path";
|
|
4580
|
+
function parseManifest(json) {
|
|
4581
|
+
const parsed = JSON.parse(json);
|
|
4582
|
+
return manifestSchema.parse(parsed);
|
|
4568
4583
|
}
|
|
4569
|
-
function
|
|
4570
|
-
|
|
4571
|
-
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4572
|
-
return [];
|
|
4573
|
-
const stem = profile ? `${name}.${profile}` : name;
|
|
4574
|
-
const configStem = profile ? `${name}-${profile}` : name;
|
|
4575
|
-
return [
|
|
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
|
-
}
|
|
4596
|
-
];
|
|
4584
|
+
function normalizeManifestName(name) {
|
|
4585
|
+
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4597
4586
|
}
|
|
4598
|
-
function
|
|
4599
|
-
return
|
|
4587
|
+
function shortManifestName(name) {
|
|
4588
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4600
4589
|
}
|
|
4601
|
-
function
|
|
4602
|
-
return
|
|
4590
|
+
function isWithin(child, parent) {
|
|
4591
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4603
4592
|
}
|
|
4604
|
-
function
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
continue;
|
|
4610
|
-
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
4611
|
-
const equals = withoutExport.indexOf("=");
|
|
4612
|
-
if (equals <= 0)
|
|
4613
|
-
continue;
|
|
4614
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
4615
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4616
|
-
continue;
|
|
4617
|
-
let value = withoutExport.slice(equals + 1).trim();
|
|
4618
|
-
const quote = value[0];
|
|
4619
|
-
if (quote === '"' || quote === "'") {
|
|
4620
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
4621
|
-
continue;
|
|
4622
|
-
value = value.slice(1, -1);
|
|
4623
|
-
}
|
|
4624
|
-
if (value.length === 0)
|
|
4625
|
-
continue;
|
|
4626
|
-
values.set(key, value);
|
|
4593
|
+
function assertContainedLexical(target, root, raw) {
|
|
4594
|
+
const lexical = resolve2(target);
|
|
4595
|
+
const rootLexical = resolve2(root);
|
|
4596
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4597
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4627
4598
|
}
|
|
4628
|
-
return
|
|
4599
|
+
return lexical;
|
|
4629
4600
|
}
|
|
4630
|
-
function
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4601
|
+
function realpathNearest(path) {
|
|
4602
|
+
const tail = [];
|
|
4603
|
+
let cur = path;
|
|
4604
|
+
while (!existsSync4(cur)) {
|
|
4605
|
+
const parent = dirname(cur);
|
|
4606
|
+
if (parent === cur)
|
|
4607
|
+
break;
|
|
4608
|
+
tail.unshift(basename(cur));
|
|
4609
|
+
cur = parent;
|
|
4639
4610
|
}
|
|
4640
|
-
return
|
|
4611
|
+
return join5(realpathSync(cur), ...tail);
|
|
4641
4612
|
}
|
|
4642
|
-
function
|
|
4643
|
-
const
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
const value = values.get(key)?.trim();
|
|
4648
|
-
if (value)
|
|
4649
|
-
return value;
|
|
4613
|
+
function assertContainedReal(root, target, raw) {
|
|
4614
|
+
const rootReal = realpathSync(root);
|
|
4615
|
+
const targetReal = realpathNearest(target);
|
|
4616
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4617
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4650
4618
|
}
|
|
4651
|
-
return null;
|
|
4652
4619
|
}
|
|
4653
|
-
function
|
|
4654
|
-
|
|
4655
|
-
|
|
4620
|
+
function assertContained(target, root, raw) {
|
|
4621
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4622
|
+
assertContainedReal(root, lexical, raw);
|
|
4623
|
+
return lexical;
|
|
4624
|
+
}
|
|
4625
|
+
function isInlineScript(manifest) {
|
|
4626
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4627
|
+
`);
|
|
4628
|
+
}
|
|
4629
|
+
function scriptRelFor(manifest) {
|
|
4630
|
+
if (isInlineScript(manifest)) {
|
|
4631
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4632
|
+
return `script${ext}`;
|
|
4656
4633
|
}
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4634
|
+
const rel = normalize(manifest.script);
|
|
4635
|
+
if (isAbsolute(rel)) {
|
|
4636
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4637
|
+
}
|
|
4638
|
+
return rel;
|
|
4660
4639
|
}
|
|
4661
|
-
function
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
4669
|
-
warning: fields.warning
|
|
4670
|
-
};
|
|
4671
|
-
const sealed = { ...visible };
|
|
4672
|
-
Object.defineProperty(sealed, "apiKey", {
|
|
4673
|
-
value: apiKey,
|
|
4674
|
-
enumerable: false,
|
|
4675
|
-
writable: false,
|
|
4676
|
-
configurable: false
|
|
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
|
-
});
|
|
4640
|
+
function resolveScript(manifest, manifestDir) {
|
|
4641
|
+
if (isInlineScript(manifest))
|
|
4642
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4643
|
+
const rel = scriptRelFor(manifest);
|
|
4644
|
+
const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
|
|
4645
|
+
if (!existsSync4(resolved)) {
|
|
4646
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4685
4647
|
}
|
|
4686
|
-
|
|
4687
|
-
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4688
|
-
enumerable: false,
|
|
4689
|
-
writable: false,
|
|
4690
|
-
configurable: false
|
|
4691
|
-
});
|
|
4692
|
-
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4693
|
-
value: true,
|
|
4694
|
-
enumerable: false,
|
|
4695
|
-
writable: false,
|
|
4696
|
-
configurable: false
|
|
4697
|
-
});
|
|
4698
|
-
return Object.freeze(sealed);
|
|
4648
|
+
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4699
4649
|
}
|
|
4700
|
-
function
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4650
|
+
function customHookDir(name) {
|
|
4651
|
+
return join5(getCustomHooksDir(), shortManifestName(name));
|
|
4652
|
+
}
|
|
4653
|
+
function readCustomManifest(name) {
|
|
4654
|
+
const dir = customHookDir(name);
|
|
4655
|
+
const manifestPath = join5(dir, "manifest.json");
|
|
4656
|
+
if (!existsSync4(manifestPath))
|
|
4657
|
+
return;
|
|
4658
|
+
try {
|
|
4659
|
+
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4660
|
+
const script = resolveScript(manifest, dir);
|
|
4661
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4662
|
+
return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4663
|
+
} catch (err) {
|
|
4664
|
+
if (err instanceof ScriptContainmentError)
|
|
4665
|
+
throw err;
|
|
4666
|
+
return;
|
|
4705
4667
|
}
|
|
4706
|
-
return null;
|
|
4707
4668
|
}
|
|
4708
|
-
function
|
|
4709
|
-
const
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
const
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
function defaultDeprecationSink(message) {
|
|
4718
|
-
if (typeof process !== "undefined" && process.stderr) {
|
|
4719
|
-
process.stderr.write(`${message}
|
|
4720
|
-
`);
|
|
4669
|
+
function listCustomHooks() {
|
|
4670
|
+
const dir = getCustomHooksDir();
|
|
4671
|
+
if (!existsSync4(dir))
|
|
4672
|
+
return [];
|
|
4673
|
+
const results = [];
|
|
4674
|
+
for (const entry of readdirSafe(dir)) {
|
|
4675
|
+
const parsed = readCustomManifest(entry);
|
|
4676
|
+
if (parsed)
|
|
4677
|
+
results.push(parsed);
|
|
4721
4678
|
}
|
|
4722
|
-
|
|
4723
|
-
function resolveCredential(name, env, options = {}) {
|
|
4724
|
-
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
4725
|
-
const diskPaths = credentialDiskSources(name, env);
|
|
4726
|
-
const explicitKey = options.apiKey?.trim();
|
|
4727
|
-
if (explicitKey) {
|
|
4728
|
-
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4729
|
-
return sealCredential({
|
|
4730
|
-
apiKey: explicitKey,
|
|
4731
|
-
tier: "argument",
|
|
4732
|
-
source: "explicit apiKey argument",
|
|
4733
|
-
deliberate: true,
|
|
4734
|
-
deprecated: false,
|
|
4735
|
-
diskCandidates: diskPaths,
|
|
4736
|
-
warning: null
|
|
4737
|
-
});
|
|
4738
|
-
}
|
|
4739
|
-
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4740
|
-
const overrideRaw = env[overrideKeyName];
|
|
4741
|
-
if (overrideRaw !== undefined) {
|
|
4742
|
-
const override = overrideRaw.trim();
|
|
4743
|
-
if (!override) {
|
|
4744
|
-
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]);
|
|
4745
|
-
}
|
|
4746
|
-
assertUsableCredential(name, overrideKeyName, override);
|
|
4747
|
-
return sealCredential({
|
|
4748
|
-
apiKey: override,
|
|
4749
|
-
tier: "override",
|
|
4750
|
-
source: overrideKeyName,
|
|
4751
|
-
deliberate: true,
|
|
4752
|
-
deprecated: false,
|
|
4753
|
-
diskCandidates: diskPaths,
|
|
4754
|
-
warning: null
|
|
4755
|
-
});
|
|
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
|
-
}
|
|
4778
|
-
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4779
|
-
if (profile) {
|
|
4780
|
-
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4781
|
-
if (!SAFE_PROFILE.test(profile)) {
|
|
4782
|
-
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4783
|
-
}
|
|
4784
|
-
const paths = profileDiskSources(name, env, profile);
|
|
4785
|
-
for (const path of paths) {
|
|
4786
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
4787
|
-
if (value) {
|
|
4788
|
-
assertUsableCredential(name, path, value);
|
|
4789
|
-
return sealCredential({
|
|
4790
|
-
apiKey: value,
|
|
4791
|
-
tier: "profile",
|
|
4792
|
-
source: path,
|
|
4793
|
-
deliberate: true,
|
|
4794
|
-
deprecated: false,
|
|
4795
|
-
diskCandidates: paths,
|
|
4796
|
-
warning: null
|
|
4797
|
-
});
|
|
4798
|
-
}
|
|
4799
|
-
}
|
|
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);
|
|
4801
|
-
}
|
|
4802
|
-
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
4803
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4804
|
-
if (diskHits.length > 0) {
|
|
4805
|
-
const winner = diskHits[0];
|
|
4806
|
-
assertUsableCredential(name, winner.src.path, winner.value);
|
|
4807
|
-
const divergentSources = [
|
|
4808
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
4809
|
-
...(() => {
|
|
4810
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4811
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4812
|
-
})()
|
|
4813
|
-
];
|
|
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
|
-
}
|
|
4830
|
-
return sealCredential({
|
|
4831
|
-
apiKey: winner.value,
|
|
4832
|
-
tier: winner.src.tier,
|
|
4833
|
-
source: winner.src.path,
|
|
4834
|
-
deliberate: false,
|
|
4835
|
-
deprecated,
|
|
4836
|
-
diskCandidates: diskPaths,
|
|
4837
|
-
warning: finalWarning
|
|
4838
|
-
});
|
|
4839
|
-
}
|
|
4840
|
-
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
4841
|
-
if (legacy) {
|
|
4842
|
-
assertUsableCredential(name, legacy.key, legacy.value);
|
|
4843
|
-
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.`;
|
|
4844
|
-
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}`;
|
|
4845
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4846
|
-
const notified = deprecationNotified();
|
|
4847
|
-
if (!notified.has(name)) {
|
|
4848
|
-
notified.add(name);
|
|
4849
|
-
sink(message);
|
|
4850
|
-
}
|
|
4851
|
-
return sealCredential({
|
|
4852
|
-
apiKey: legacy.value,
|
|
4853
|
-
tier: "legacy-env",
|
|
4854
|
-
source: legacy.key,
|
|
4855
|
-
deliberate: false,
|
|
4856
|
-
deprecated: true,
|
|
4857
|
-
diskCandidates: diskPaths,
|
|
4858
|
-
warning: message
|
|
4859
|
-
});
|
|
4860
|
-
}
|
|
4861
|
-
return null;
|
|
4862
|
-
}
|
|
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;
|
|
4864
|
-
var init_transport = __esm(() => {
|
|
4865
|
-
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
4866
|
-
appName;
|
|
4867
|
-
attempted;
|
|
4868
|
-
constructor(appName, message, attempted) {
|
|
4869
|
-
super(message);
|
|
4870
|
-
this.name = "CredentialResolutionError";
|
|
4871
|
-
this.appName = appName;
|
|
4872
|
-
this.attempted = attempted;
|
|
4873
|
-
}
|
|
4874
|
-
};
|
|
4875
|
-
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4876
|
-
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4877
|
-
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
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,}$/;
|
|
4880
|
-
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4881
|
-
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4882
|
-
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4883
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
4884
|
-
requireSecretsSdk = createRequire(import.meta.url);
|
|
4885
|
-
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4886
|
-
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4887
|
-
"host",
|
|
4888
|
-
":authority",
|
|
4889
|
-
"forwarded",
|
|
4890
|
-
"x-forwarded-host",
|
|
4891
|
-
"x-original-host"
|
|
4892
|
-
]);
|
|
4893
|
-
});
|
|
4894
|
-
|
|
4895
|
-
// src/config.ts
|
|
4896
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
4897
|
-
import { join as join6 } from "path";
|
|
4898
|
-
function getHooksDataDir() {
|
|
4899
|
-
return getEffectiveDataRoot();
|
|
4900
|
-
}
|
|
4901
|
-
function getCustomHooksDir() {
|
|
4902
|
-
return join6(getHooksDataDir(), "hooks");
|
|
4903
|
-
}
|
|
4904
|
-
function getLockPath() {
|
|
4905
|
-
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4906
|
-
if (explicit)
|
|
4907
|
-
return explicit;
|
|
4908
|
-
return join6(getHooksDataDir(), "hooks.lock");
|
|
4909
|
-
}
|
|
4910
|
-
function getConfigPath() {
|
|
4911
|
-
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4912
|
-
if (explicit)
|
|
4913
|
-
return explicit;
|
|
4914
|
-
return join6(getHooksDataDir(), "config.json");
|
|
4915
|
-
}
|
|
4916
|
-
function readConfig() {
|
|
4917
|
-
try {
|
|
4918
|
-
const path = getConfigPath();
|
|
4919
|
-
if (!existsSync4(path))
|
|
4920
|
-
return {};
|
|
4921
|
-
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
4922
|
-
} catch {
|
|
4923
|
-
return {};
|
|
4924
|
-
}
|
|
4925
|
-
}
|
|
4926
|
-
function writeConfig(config) {
|
|
4927
|
-
const path = getConfigPath();
|
|
4928
|
-
mkdirSync2(join6(path, ".."), { recursive: true });
|
|
4929
|
-
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4930
|
-
`, "utf-8");
|
|
4931
|
-
return path;
|
|
4932
|
-
}
|
|
4933
|
-
function resolveApiUrl() {
|
|
4934
|
-
const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
|
|
4935
|
-
if (envUrl)
|
|
4936
|
-
return envUrl.replace(/\/+$/, "");
|
|
4937
|
-
const cfg = readConfig();
|
|
4938
|
-
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4939
|
-
}
|
|
4940
|
-
function resolveApiKey() {
|
|
4941
|
-
return resolveCredential("hooks", process.env)?.apiKey;
|
|
4942
|
-
}
|
|
4943
|
-
var init_config = __esm(() => {
|
|
4944
|
-
init_transport();
|
|
4945
|
-
init_app_home();
|
|
4946
|
-
});
|
|
4947
|
-
|
|
4948
|
-
// src/lib/semver.ts
|
|
4949
|
-
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4950
|
-
var init_semver = __esm(() => {
|
|
4951
|
-
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4952
|
-
});
|
|
4953
|
-
|
|
4954
|
-
// src/lib/manifest.ts
|
|
4955
|
-
var exports_manifest = {};
|
|
4956
|
-
__export(exports_manifest, {
|
|
4957
|
-
writeCustomHook: () => writeCustomHook,
|
|
4958
|
-
shortManifestName: () => shortManifestName,
|
|
4959
|
-
scriptRelFor: () => scriptRelFor,
|
|
4960
|
-
resolveScript: () => resolveScript,
|
|
4961
|
-
readCustomManifest: () => readCustomManifest,
|
|
4962
|
-
parseManifest: () => parseManifest,
|
|
4963
|
-
normalizeManifestName: () => normalizeManifestName,
|
|
4964
|
-
manifestSchema: () => manifestSchema,
|
|
4965
|
-
listCustomHooks: () => listCustomHooks,
|
|
4966
|
-
isInlineScript: () => isInlineScript,
|
|
4967
|
-
customHookDir: () => customHookDir,
|
|
4968
|
-
assertContained: () => assertContained,
|
|
4969
|
-
ScriptContainmentError: () => ScriptContainmentError,
|
|
4970
|
-
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4971
|
-
});
|
|
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";
|
|
4974
|
-
function parseManifest(json) {
|
|
4975
|
-
const parsed = JSON.parse(json);
|
|
4976
|
-
return manifestSchema.parse(parsed);
|
|
4977
|
-
}
|
|
4978
|
-
function normalizeManifestName(name) {
|
|
4979
|
-
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4980
|
-
}
|
|
4981
|
-
function shortManifestName(name) {
|
|
4982
|
-
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4983
|
-
}
|
|
4984
|
-
function isWithin(child, parent) {
|
|
4985
|
-
return child === parent || child.startsWith(parent + sep);
|
|
4986
|
-
}
|
|
4987
|
-
function assertContainedLexical(target, root, raw) {
|
|
4988
|
-
const lexical = resolve2(target);
|
|
4989
|
-
const rootLexical = resolve2(root);
|
|
4990
|
-
if (!isWithin(lexical, rootLexical)) {
|
|
4991
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
4992
|
-
}
|
|
4993
|
-
return lexical;
|
|
4994
|
-
}
|
|
4995
|
-
function realpathNearest(path) {
|
|
4996
|
-
const tail = [];
|
|
4997
|
-
let cur = path;
|
|
4998
|
-
while (!existsSync5(cur)) {
|
|
4999
|
-
const parent = dirname(cur);
|
|
5000
|
-
if (parent === cur)
|
|
5001
|
-
break;
|
|
5002
|
-
tail.unshift(basename(cur));
|
|
5003
|
-
cur = parent;
|
|
5004
|
-
}
|
|
5005
|
-
return join7(realpathSync(cur), ...tail);
|
|
5006
|
-
}
|
|
5007
|
-
function assertContainedReal(root, target, raw) {
|
|
5008
|
-
const rootReal = realpathSync(root);
|
|
5009
|
-
const targetReal = realpathNearest(target);
|
|
5010
|
-
if (!isWithin(targetReal, rootReal)) {
|
|
5011
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
5012
|
-
}
|
|
5013
|
-
}
|
|
5014
|
-
function assertContained(target, root, raw) {
|
|
5015
|
-
const lexical = assertContainedLexical(target, root, raw);
|
|
5016
|
-
assertContainedReal(root, lexical, raw);
|
|
5017
|
-
return lexical;
|
|
5018
|
-
}
|
|
5019
|
-
function isInlineScript(manifest) {
|
|
5020
|
-
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
5021
|
-
`);
|
|
5022
|
-
}
|
|
5023
|
-
function scriptRelFor(manifest) {
|
|
5024
|
-
if (isInlineScript(manifest)) {
|
|
5025
|
-
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
5026
|
-
return `script${ext}`;
|
|
5027
|
-
}
|
|
5028
|
-
const rel = normalize(manifest.script);
|
|
5029
|
-
if (isAbsolute(rel)) {
|
|
5030
|
-
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
5031
|
-
}
|
|
5032
|
-
return rel;
|
|
5033
|
-
}
|
|
5034
|
-
function resolveScript(manifest, manifestDir) {
|
|
5035
|
-
if (isInlineScript(manifest))
|
|
5036
|
-
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
5037
|
-
const rel = scriptRelFor(manifest);
|
|
5038
|
-
const resolved = assertContained(join7(manifestDir, rel), manifestDir, manifest.script);
|
|
5039
|
-
if (!existsSync5(resolved)) {
|
|
5040
|
-
throw new Error(`manifest script file not found: ${resolved}`);
|
|
5041
|
-
}
|
|
5042
|
-
return { path: rel, content: readFileSync4(resolved, "utf-8") };
|
|
5043
|
-
}
|
|
5044
|
-
function customHookDir(name) {
|
|
5045
|
-
return join7(getCustomHooksDir(), shortManifestName(name));
|
|
5046
|
-
}
|
|
5047
|
-
function readCustomManifest(name) {
|
|
5048
|
-
const dir = customHookDir(name);
|
|
5049
|
-
const manifestPath = join7(dir, "manifest.json");
|
|
5050
|
-
if (!existsSync5(manifestPath))
|
|
5051
|
-
return;
|
|
5052
|
-
try {
|
|
5053
|
-
const manifest = parseManifest(readFileSync4(manifestPath, "utf-8"));
|
|
5054
|
-
const script = resolveScript(manifest, dir);
|
|
5055
|
-
const scriptIsInline = isInlineScript(manifest);
|
|
5056
|
-
return { manifest, scriptPath: join7(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
5057
|
-
} catch (err) {
|
|
5058
|
-
if (err instanceof ScriptContainmentError)
|
|
5059
|
-
throw err;
|
|
5060
|
-
return;
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
function listCustomHooks() {
|
|
5064
|
-
const dir = getCustomHooksDir();
|
|
5065
|
-
if (!existsSync5(dir))
|
|
5066
|
-
return [];
|
|
5067
|
-
const results = [];
|
|
5068
|
-
for (const entry of readdirSafe(dir)) {
|
|
5069
|
-
const parsed = readCustomManifest(entry);
|
|
5070
|
-
if (parsed)
|
|
5071
|
-
results.push(parsed);
|
|
5072
|
-
}
|
|
5073
|
-
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4679
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
5074
4680
|
}
|
|
5075
4681
|
function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
5076
4682
|
const dir = customHookDir(name);
|
|
5077
4683
|
if (isAbsolute(scriptRelPath)) {
|
|
5078
4684
|
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
5079
4685
|
}
|
|
5080
|
-
const scriptPath = scriptRelPath.includes("/") ?
|
|
4686
|
+
const scriptPath = scriptRelPath.includes("/") ? join5(dir, scriptRelPath) : join5(dir, scriptRelPath);
|
|
5081
4687
|
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
5082
|
-
|
|
4688
|
+
mkdirSync2(dir, { recursive: true });
|
|
5083
4689
|
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
5084
4690
|
const scriptDir = dirname(scriptPath);
|
|
5085
4691
|
if (scriptDir !== dir)
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
4692
|
+
mkdirSync2(scriptDir, { recursive: true });
|
|
4693
|
+
writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
4694
|
+
writeFileSync(join5(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
5089
4695
|
`, "utf-8");
|
|
5090
4696
|
return { dir, scriptPath };
|
|
5091
4697
|
}
|
|
@@ -5143,7 +4749,7 @@ __export(exports_store, {
|
|
|
5143
4749
|
LockFileError: () => LockFileError
|
|
5144
4750
|
});
|
|
5145
4751
|
import { createHash } from "crypto";
|
|
5146
|
-
import { existsSync as
|
|
4752
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5147
4753
|
import { readFile } from "fs/promises";
|
|
5148
4754
|
import { randomBytes } from "crypto";
|
|
5149
4755
|
function sha256Of(input) {
|
|
@@ -5205,11 +4811,11 @@ function assertLockShape(parsed, path) {
|
|
|
5205
4811
|
}
|
|
5206
4812
|
function readLock() {
|
|
5207
4813
|
const path = getLockPath();
|
|
5208
|
-
if (!
|
|
4814
|
+
if (!existsSync6(path))
|
|
5209
4815
|
return { hooks: {} };
|
|
5210
4816
|
let raw;
|
|
5211
4817
|
try {
|
|
5212
|
-
raw =
|
|
4818
|
+
raw = readFileSync3(path, "utf-8");
|
|
5213
4819
|
} catch {
|
|
5214
4820
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5215
4821
|
}
|
|
@@ -5224,13 +4830,13 @@ function readLock() {
|
|
|
5224
4830
|
}
|
|
5225
4831
|
function writeLock(lock) {
|
|
5226
4832
|
const path = getLockPath();
|
|
5227
|
-
|
|
4833
|
+
mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5228
4834
|
const sorted = {};
|
|
5229
4835
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5230
4836
|
sorted[name] = lock.hooks[name];
|
|
5231
4837
|
}
|
|
5232
4838
|
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
5233
|
-
|
|
4839
|
+
writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5234
4840
|
`, "utf-8");
|
|
5235
4841
|
renameSync(tmp, path);
|
|
5236
4842
|
return path;
|
|
@@ -5306,7 +4912,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5306
4912
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5307
4913
|
}
|
|
5308
4914
|
function retrustHook(name, scriptPath, version, source) {
|
|
5309
|
-
const actual = sha256Of(
|
|
4915
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5310
4916
|
const db = getDb();
|
|
5311
4917
|
upsertHookRecord(db, {
|
|
5312
4918
|
name,
|
|
@@ -5343,31 +4949,29 @@ __export(exports_profiles, {
|
|
|
5343
4949
|
deleteProfile: () => deleteProfile,
|
|
5344
4950
|
createProfile: () => createProfile
|
|
5345
4951
|
});
|
|
5346
|
-
import { existsSync as
|
|
5347
|
-
import { join as
|
|
5348
|
-
function
|
|
5349
|
-
const newDir =
|
|
5350
|
-
const oldDir =
|
|
5351
|
-
if (!
|
|
5352
|
-
|
|
4952
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
4953
|
+
import { join as join8 } from "path";
|
|
4954
|
+
function getProfilesDir() {
|
|
4955
|
+
const newDir = join8(getEffectiveDataRoot(), "profiles");
|
|
4956
|
+
const oldDir = join8(getHomeDir(), ".hooks", "profiles");
|
|
4957
|
+
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
4958
|
+
mkdirSync5(newDir, { recursive: true });
|
|
5353
4959
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5354
4960
|
}
|
|
5355
4961
|
return newDir;
|
|
5356
4962
|
}
|
|
5357
4963
|
function ensureProfilesDir() {
|
|
5358
|
-
|
|
5359
|
-
|
|
4964
|
+
const dir = getProfilesDir();
|
|
4965
|
+
if (!existsSync8(dir)) {
|
|
4966
|
+
mkdirSync5(dir, { recursive: true });
|
|
5360
4967
|
}
|
|
5361
4968
|
}
|
|
5362
4969
|
function profilePath(id) {
|
|
5363
|
-
return
|
|
4970
|
+
return join8(getProfilesDir(), `${id}.json`);
|
|
5364
4971
|
}
|
|
5365
4972
|
function shortUuid() {
|
|
5366
4973
|
return crypto.randomUUID().slice(0, 8);
|
|
5367
4974
|
}
|
|
5368
|
-
function getProfilesDir() {
|
|
5369
|
-
return PROFILES_DIR;
|
|
5370
|
-
}
|
|
5371
4975
|
function createProfile(input) {
|
|
5372
4976
|
ensureProfilesDir();
|
|
5373
4977
|
const id = shortUuid();
|
|
@@ -5382,29 +4986,30 @@ function createProfile(input) {
|
|
|
5382
4986
|
if (input.name) {
|
|
5383
4987
|
profile.name = input.name;
|
|
5384
4988
|
}
|
|
5385
|
-
|
|
4989
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5386
4990
|
`);
|
|
5387
4991
|
return profile;
|
|
5388
4992
|
}
|
|
5389
4993
|
function getProfile(id) {
|
|
5390
4994
|
const path = profilePath(id);
|
|
5391
4995
|
try {
|
|
5392
|
-
if (!
|
|
4996
|
+
if (!existsSync8(path))
|
|
5393
4997
|
return null;
|
|
5394
|
-
return JSON.parse(
|
|
4998
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5395
4999
|
} catch {
|
|
5396
5000
|
return null;
|
|
5397
5001
|
}
|
|
5398
5002
|
}
|
|
5399
5003
|
function listProfiles() {
|
|
5400
|
-
|
|
5004
|
+
const dir = getProfilesDir();
|
|
5005
|
+
if (!existsSync8(dir))
|
|
5401
5006
|
return [];
|
|
5402
5007
|
try {
|
|
5403
|
-
const files =
|
|
5008
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
5404
5009
|
const profiles = [];
|
|
5405
5010
|
for (const file of files) {
|
|
5406
5011
|
try {
|
|
5407
|
-
const content =
|
|
5012
|
+
const content = readFileSync5(join8(dir, file), "utf-8");
|
|
5408
5013
|
profiles.push(JSON.parse(content));
|
|
5409
5014
|
} catch {}
|
|
5410
5015
|
}
|
|
@@ -5421,16 +5026,16 @@ function updateProfile(id, data) {
|
|
|
5421
5026
|
profile.name = data.name;
|
|
5422
5027
|
if (data.preferences !== undefined)
|
|
5423
5028
|
profile.preferences = data.preferences;
|
|
5424
|
-
|
|
5029
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5425
5030
|
`);
|
|
5426
5031
|
return profile;
|
|
5427
5032
|
}
|
|
5428
5033
|
function deleteProfile(id) {
|
|
5429
5034
|
const path = profilePath(id);
|
|
5430
|
-
if (!
|
|
5035
|
+
if (!existsSync8(path))
|
|
5431
5036
|
return false;
|
|
5432
5037
|
try {
|
|
5433
|
-
|
|
5038
|
+
rmSync(path);
|
|
5434
5039
|
return true;
|
|
5435
5040
|
} catch {
|
|
5436
5041
|
return false;
|
|
@@ -5441,7 +5046,7 @@ function touchProfile(id) {
|
|
|
5441
5046
|
if (!profile)
|
|
5442
5047
|
return;
|
|
5443
5048
|
profile.last_seen_at = new Date().toISOString();
|
|
5444
|
-
|
|
5049
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5445
5050
|
`);
|
|
5446
5051
|
}
|
|
5447
5052
|
function exportProfiles() {
|
|
@@ -5457,25 +5062,23 @@ function importProfiles(profiles) {
|
|
|
5457
5062
|
continue;
|
|
5458
5063
|
}
|
|
5459
5064
|
const path = profilePath(profile.agent_id);
|
|
5460
|
-
if (
|
|
5065
|
+
if (existsSync8(path)) {
|
|
5461
5066
|
skipped++;
|
|
5462
5067
|
continue;
|
|
5463
5068
|
}
|
|
5464
|
-
|
|
5069
|
+
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
5465
5070
|
`);
|
|
5466
5071
|
imported++;
|
|
5467
5072
|
}
|
|
5468
5073
|
return { imported, skipped };
|
|
5469
5074
|
}
|
|
5470
|
-
var PROFILES_DIR;
|
|
5471
5075
|
var init_profiles = __esm(() => {
|
|
5472
5076
|
init_app_home();
|
|
5473
|
-
PROFILES_DIR = resolveProfilesDir();
|
|
5474
5077
|
});
|
|
5475
5078
|
|
|
5476
5079
|
// src/lib/hook-env.ts
|
|
5477
|
-
import { dirname as dirname5, isAbsolute as
|
|
5478
|
-
import { existsSync as
|
|
5080
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
5081
|
+
import { existsSync as existsSync10, statSync } from "fs";
|
|
5479
5082
|
function isDeniedEnvName(name) {
|
|
5480
5083
|
const upper = name.toUpperCase();
|
|
5481
5084
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -5493,7 +5096,7 @@ function isBashFunctionEnvName(name) {
|
|
|
5493
5096
|
return name.startsWith("BASH_FUNC_");
|
|
5494
5097
|
}
|
|
5495
5098
|
function isUnsafePathEntry(entry, home) {
|
|
5496
|
-
if (entry === "" || !
|
|
5099
|
+
if (entry === "" || !isAbsolute3(entry))
|
|
5497
5100
|
return true;
|
|
5498
5101
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
5499
5102
|
return true;
|
|
@@ -5502,14 +5105,14 @@ function isUnsafePathEntry(entry, home) {
|
|
|
5502
5105
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
5503
5106
|
return true;
|
|
5504
5107
|
try {
|
|
5505
|
-
if ((
|
|
5108
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
5506
5109
|
return true;
|
|
5507
5110
|
} catch {}
|
|
5508
5111
|
return false;
|
|
5509
5112
|
}
|
|
5510
5113
|
function sanitizeHookPath(pathValue, home) {
|
|
5511
5114
|
const baseline = PATH_BASELINE.slice();
|
|
5512
|
-
if (process.platform === "darwin" &&
|
|
5115
|
+
if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
|
|
5513
5116
|
baseline.push("/opt/homebrew/bin");
|
|
5514
5117
|
const bunDir = dirname5(process.execPath);
|
|
5515
5118
|
const seen = new Set;
|
|
@@ -5634,8 +5237,8 @@ __export(exports_run, {
|
|
|
5634
5237
|
HookTimeoutError: () => HookTimeoutError
|
|
5635
5238
|
});
|
|
5636
5239
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5637
|
-
import { rmSync as
|
|
5638
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as
|
|
5240
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
5241
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
5639
5242
|
function extensionOf(scriptPath) {
|
|
5640
5243
|
const base = basename2(scriptPath);
|
|
5641
5244
|
const dot = base.lastIndexOf(".");
|
|
@@ -5706,7 +5309,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
5706
5309
|
async function executeVerifiedScript(options) {
|
|
5707
5310
|
const scriptDir = dirname6(options.scriptPath);
|
|
5708
5311
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5709
|
-
const tempPath =
|
|
5312
|
+
const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5710
5313
|
let proc = null;
|
|
5711
5314
|
let timedOut = false;
|
|
5712
5315
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -5714,8 +5317,8 @@ async function executeVerifiedScript(options) {
|
|
|
5714
5317
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
5715
5318
|
}, options.timeout) : null;
|
|
5716
5319
|
try {
|
|
5717
|
-
|
|
5718
|
-
proc = Bun.spawn([...interpreter.command,
|
|
5320
|
+
writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5321
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5719
5322
|
stdin: new Response(options.stdin),
|
|
5720
5323
|
stdout: "pipe",
|
|
5721
5324
|
stderr: "pipe",
|
|
@@ -5736,7 +5339,7 @@ async function executeVerifiedScript(options) {
|
|
|
5736
5339
|
if (timeoutTimer)
|
|
5737
5340
|
clearTimeout(timeoutTimer);
|
|
5738
5341
|
try {
|
|
5739
|
-
|
|
5342
|
+
rmSync3(tempPath, { force: true });
|
|
5740
5343
|
} catch {}
|
|
5741
5344
|
}
|
|
5742
5345
|
}
|
|
@@ -6871,21 +6474,21 @@ function getHook(name) {
|
|
|
6871
6474
|
return HOOKS.find((h) => h.name === name);
|
|
6872
6475
|
}
|
|
6873
6476
|
// src/lib/installer.ts
|
|
6874
|
-
import { existsSync as
|
|
6875
|
-
import { join as
|
|
6876
|
-
import { homedir as
|
|
6477
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
6478
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
6479
|
+
import { homedir as homedir3 } from "os";
|
|
6877
6480
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6878
6481
|
|
|
6879
6482
|
// src/lib/resolve.ts
|
|
6880
|
-
import { dirname as dirname2, join as
|
|
6881
|
-
import { existsSync as
|
|
6483
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
6484
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6882
6485
|
import { fileURLToPath } from "url";
|
|
6883
6486
|
init_manifest();
|
|
6884
6487
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
6885
|
-
const sourceLayout =
|
|
6886
|
-
if (
|
|
6488
|
+
const sourceLayout = join6(moduleDir, "..", "..", "hooks");
|
|
6489
|
+
if (existsSync5(join6(sourceLayout, "hook-gitguard")))
|
|
6887
6490
|
return sourceLayout;
|
|
6888
|
-
return
|
|
6491
|
+
return join6(moduleDir, "..", "hooks");
|
|
6889
6492
|
}
|
|
6890
6493
|
function resolveHookDir(name) {
|
|
6891
6494
|
const custom2 = readCustomManifest(name);
|
|
@@ -6896,11 +6499,11 @@ function resolveHookDir(name) {
|
|
|
6896
6499
|
return;
|
|
6897
6500
|
const short = shortManifestName(name);
|
|
6898
6501
|
const bundledBase = resolveBundledHooksDir();
|
|
6899
|
-
const direct =
|
|
6900
|
-
if (
|
|
6502
|
+
const direct = join6(bundledBase, short);
|
|
6503
|
+
if (existsSync5(direct))
|
|
6901
6504
|
return direct;
|
|
6902
|
-
const prefixed =
|
|
6903
|
-
return
|
|
6505
|
+
const prefixed = join6(bundledBase, `hook-${short}`);
|
|
6506
|
+
return existsSync5(prefixed) ? prefixed : undefined;
|
|
6904
6507
|
}
|
|
6905
6508
|
function resolveScriptPath(name) {
|
|
6906
6509
|
const custom2 = readCustomManifest(name);
|
|
@@ -6909,8 +6512,8 @@ function resolveScriptPath(name) {
|
|
|
6909
6512
|
const dir = resolveHookDir(name);
|
|
6910
6513
|
if (!dir)
|
|
6911
6514
|
return;
|
|
6912
|
-
const script =
|
|
6913
|
-
return
|
|
6515
|
+
const script = join6(dir, "src", "hook.ts");
|
|
6516
|
+
return existsSync5(script) ? script : undefined;
|
|
6914
6517
|
}
|
|
6915
6518
|
function resolveHookMeta(name) {
|
|
6916
6519
|
const custom2 = readCustomManifest(name);
|
|
@@ -6965,7 +6568,7 @@ function resolveHook(name) {
|
|
|
6965
6568
|
init_manifest();
|
|
6966
6569
|
init_store();
|
|
6967
6570
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
6968
|
-
var HOOKS_DIR =
|
|
6571
|
+
var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
|
|
6969
6572
|
function normalizeHookName(name) {
|
|
6970
6573
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
6971
6574
|
}
|
|
@@ -7035,25 +6638,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
7035
6638
|
return globalOverride;
|
|
7036
6639
|
const dir = getTargetSettingsDir(target);
|
|
7037
6640
|
if (scope === "project") {
|
|
7038
|
-
return target === "codewith" ?
|
|
6641
|
+
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
7039
6642
|
}
|
|
7040
|
-
return target === "codewith" ?
|
|
6643
|
+
return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
|
|
7041
6644
|
}
|
|
7042
6645
|
function getHookPath(name) {
|
|
7043
6646
|
const shortName = shortHookName(name);
|
|
7044
6647
|
const resolved = resolveHookDir(shortName);
|
|
7045
6648
|
if (resolved)
|
|
7046
6649
|
return resolved;
|
|
7047
|
-
return
|
|
6650
|
+
return join7(HOOKS_DIR, normalizeHookName(shortName));
|
|
7048
6651
|
}
|
|
7049
6652
|
function hookExists(name) {
|
|
7050
|
-
return
|
|
6653
|
+
return existsSync7(getHookPath(name));
|
|
7051
6654
|
}
|
|
7052
6655
|
function readSettings(scope = "global", target = "claude") {
|
|
7053
6656
|
const path = getSettingsPath(scope, target);
|
|
7054
6657
|
try {
|
|
7055
|
-
if (
|
|
7056
|
-
return JSON.parse(
|
|
6658
|
+
if (existsSync7(path)) {
|
|
6659
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
7057
6660
|
}
|
|
7058
6661
|
} catch (error) {
|
|
7059
6662
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -7063,10 +6666,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
7063
6666
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
7064
6667
|
const path = getSettingsPath(scope, target);
|
|
7065
6668
|
const dir = dirname3(path);
|
|
7066
|
-
if (!
|
|
7067
|
-
|
|
6669
|
+
if (!existsSync7(dir)) {
|
|
6670
|
+
mkdirSync4(dir, { recursive: true });
|
|
7068
6671
|
}
|
|
7069
|
-
|
|
6672
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
7070
6673
|
`);
|
|
7071
6674
|
}
|
|
7072
6675
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -7152,7 +6755,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
7152
6755
|
function readCodewithConfig(scope, configPath) {
|
|
7153
6756
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7154
6757
|
try {
|
|
7155
|
-
return
|
|
6758
|
+
return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
7156
6759
|
} catch {
|
|
7157
6760
|
return "";
|
|
7158
6761
|
}
|
|
@@ -7160,13 +6763,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
7160
6763
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
7161
6764
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7162
6765
|
const dir = dirname3(path);
|
|
7163
|
-
if (!
|
|
7164
|
-
|
|
7165
|
-
const existing =
|
|
6766
|
+
if (!existsSync7(dir))
|
|
6767
|
+
mkdirSync4(dir, { recursive: true });
|
|
6768
|
+
const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
7166
6769
|
const sep2 = existing.trim() ? `
|
|
7167
6770
|
|
|
7168
6771
|
` : "";
|
|
7169
|
-
|
|
6772
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
7170
6773
|
return path;
|
|
7171
6774
|
}
|
|
7172
6775
|
function detectConflict(name, scope, target) {
|
|
@@ -7463,16 +7066,30 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
7463
7066
|
}
|
|
7464
7067
|
function codewithHasHookEntry(name, scope = "global") {
|
|
7465
7068
|
const path = getSettingsPath(scope, "codewith");
|
|
7466
|
-
if (!
|
|
7069
|
+
if (!existsSync7(path))
|
|
7467
7070
|
return false;
|
|
7468
7071
|
try {
|
|
7469
|
-
return codewithHasHookEntryInText(
|
|
7072
|
+
return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
|
|
7470
7073
|
} catch {
|
|
7471
7074
|
return false;
|
|
7472
7075
|
}
|
|
7473
7076
|
}
|
|
7474
|
-
function
|
|
7475
|
-
const
|
|
7077
|
+
function removeStoreDirFailClosed(dir) {
|
|
7078
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
7079
|
+
for (const entry of entries) {
|
|
7080
|
+
if (entry.isDirectory()) {
|
|
7081
|
+
removeStoreDirFailClosed(join7(dir, entry.name));
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
for (const entry of entries) {
|
|
7085
|
+
if (!entry.isDirectory()) {
|
|
7086
|
+
unlinkSync(join7(dir, entry.name));
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
rmdirSync(dir);
|
|
7090
|
+
}
|
|
7091
|
+
function uninstallHook(name, scope = "global", target = "claude") {
|
|
7092
|
+
const shortName = shortHookName(name);
|
|
7476
7093
|
const custom2 = readCustomManifest(shortName);
|
|
7477
7094
|
const bundledMeta = custom2 ? undefined : getHook(shortName);
|
|
7478
7095
|
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
@@ -7497,11 +7114,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7497
7114
|
const registrationsRemaining = [];
|
|
7498
7115
|
if (target === "codewith" || target === "all") {
|
|
7499
7116
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
7500
|
-
if (
|
|
7501
|
-
const before =
|
|
7117
|
+
if (existsSync7(codewithPath)) {
|
|
7118
|
+
const before = readFileSync4(codewithPath, "utf-8");
|
|
7502
7119
|
const after = removeCodewithHookEntry(before, shortName);
|
|
7503
7120
|
if (after.removed) {
|
|
7504
|
-
|
|
7121
|
+
writeFileSync3(codewithPath, after.text, "utf-8");
|
|
7505
7122
|
}
|
|
7506
7123
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
7507
7124
|
registrationsRemaining.push("codewith");
|
|
@@ -7510,7 +7127,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7510
7127
|
}
|
|
7511
7128
|
if (custom2) {
|
|
7512
7129
|
try {
|
|
7513
|
-
|
|
7130
|
+
removeStoreDirFailClosed(customHookDir(shortName));
|
|
7514
7131
|
} catch {
|
|
7515
7132
|
return {
|
|
7516
7133
|
name: shortName,
|
|
@@ -7540,22 +7157,891 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7540
7157
|
// src/index.ts
|
|
7541
7158
|
init_profiles();
|
|
7542
7159
|
init_config();
|
|
7160
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
7161
|
+
|
|
7162
|
+
// ../contracts/dist/client/transport.js
|
|
7163
|
+
import { isIP } from "net";
|
|
7164
|
+
import { spawnSync } from "child_process";
|
|
7165
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
7166
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
7167
|
+
import { createRequire } from "module";
|
|
7168
|
+
import { hostname as osHostname } from "os";
|
|
7169
|
+
import { isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
7170
|
+
function envToken(name) {
|
|
7171
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
7172
|
+
}
|
|
7173
|
+
function clientTransportEnvKeys(name) {
|
|
7174
|
+
const envSegment = envToken(name);
|
|
7175
|
+
return {
|
|
7176
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
7177
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
7178
|
+
};
|
|
7179
|
+
}
|
|
7180
|
+
function credentialOverrideEnvKey(name) {
|
|
7181
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
7182
|
+
}
|
|
7183
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
7184
|
+
function credentialPointerEnvKey(name) {
|
|
7185
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
7186
|
+
}
|
|
7187
|
+
|
|
7188
|
+
class CredentialResolutionError extends Error {
|
|
7189
|
+
appName;
|
|
7190
|
+
attempted;
|
|
7191
|
+
constructor(appName, message, attempted) {
|
|
7192
|
+
super(message);
|
|
7193
|
+
this.name = "CredentialResolutionError";
|
|
7194
|
+
this.appName = appName;
|
|
7195
|
+
this.attempted = attempted;
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
class CredentialFileUnsafeError extends Error {
|
|
7200
|
+
path;
|
|
7201
|
+
constructor(path, reason) {
|
|
7202
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
7203
|
+
this.name = "CredentialFileUnsafeError";
|
|
7204
|
+
this.path = path;
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
7208
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
7209
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
7210
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
7211
|
+
var CONFIG_SUBDIR = "config";
|
|
7212
|
+
var CREDENTIALS_FILE = "credentials";
|
|
7213
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
7214
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
7215
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
7216
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
7217
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
7218
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7219
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
7220
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
7221
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
7222
|
+
function homeDir(env) {
|
|
7223
|
+
const home = env.HOME?.trim();
|
|
7224
|
+
return home ? home : null;
|
|
7225
|
+
}
|
|
7226
|
+
function absoluteOverride(env, key) {
|
|
7227
|
+
const value = env[key]?.trim();
|
|
7228
|
+
return value && isAbsolute2(value) ? value : null;
|
|
7229
|
+
}
|
|
7230
|
+
function hasnaHomeDir(env) {
|
|
7231
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
7232
|
+
if (override)
|
|
7233
|
+
return override;
|
|
7234
|
+
const home = homeDir(env);
|
|
7235
|
+
return home ? join9(home, HASNA_HOME_DIR) : null;
|
|
7236
|
+
}
|
|
7237
|
+
function appConfigDir(name, env) {
|
|
7238
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
7239
|
+
if (configRoot)
|
|
7240
|
+
return join9(configRoot, name);
|
|
7241
|
+
const root = hasnaHomeDir(env);
|
|
7242
|
+
return root ? join9(root, name, CONFIG_SUBDIR) : null;
|
|
7243
|
+
}
|
|
7244
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
7245
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
7246
|
+
return [];
|
|
7247
|
+
const directory = appConfigDir(name, env);
|
|
7248
|
+
if (!directory)
|
|
7249
|
+
return [];
|
|
7250
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
7251
|
+
return [{ path: join9(directory, file), tier: "disk" }];
|
|
7252
|
+
}
|
|
7253
|
+
function credentialDiskSources(name, env) {
|
|
7254
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
7255
|
+
}
|
|
7256
|
+
function profileDiskSources(name, env, profile) {
|
|
7257
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
7258
|
+
}
|
|
7259
|
+
function parseEnvFile(text) {
|
|
7260
|
+
const values = new Map;
|
|
7261
|
+
const unusable = new Set;
|
|
7262
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
7263
|
+
const line = rawLine.trim();
|
|
7264
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
7265
|
+
continue;
|
|
7266
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
7267
|
+
const equals = withoutExport.indexOf("=");
|
|
7268
|
+
if (equals <= 0)
|
|
7269
|
+
continue;
|
|
7270
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
7271
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
7272
|
+
continue;
|
|
7273
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
7274
|
+
const quote = value[0];
|
|
7275
|
+
if (quote === '"' || quote === "'") {
|
|
7276
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
7277
|
+
unusable.add(key);
|
|
7278
|
+
continue;
|
|
7279
|
+
}
|
|
7280
|
+
value = value.slice(1, -1);
|
|
7281
|
+
}
|
|
7282
|
+
if (value.trim().length === 0) {
|
|
7283
|
+
unusable.add(key);
|
|
7284
|
+
continue;
|
|
7285
|
+
}
|
|
7286
|
+
if (values.has(key) && values.get(key) !== value)
|
|
7287
|
+
unusable.add(key);
|
|
7288
|
+
values.set(key, value);
|
|
7289
|
+
}
|
|
7290
|
+
return { values, unusable };
|
|
7291
|
+
}
|
|
7292
|
+
function configFileModeAllowed(mode) {
|
|
7293
|
+
const permissions = mode & 4095;
|
|
7294
|
+
return permissions === 256 || permissions === 384;
|
|
7295
|
+
}
|
|
7296
|
+
function configFileReadsCoherent(before, after) {
|
|
7297
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
7298
|
+
}
|
|
7299
|
+
function readAppConfigFile(path) {
|
|
7300
|
+
const unsafe = (reason) => {
|
|
7301
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
7302
|
+
};
|
|
7303
|
+
let fd = -1;
|
|
7304
|
+
try {
|
|
7305
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
7306
|
+
} catch (error) {
|
|
7307
|
+
const code = error.code;
|
|
7308
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
7309
|
+
return null;
|
|
7310
|
+
if (code === "ELOOP")
|
|
7311
|
+
unsafe("the path is a symlink");
|
|
7312
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
7313
|
+
}
|
|
7314
|
+
try {
|
|
7315
|
+
const before = fstatSync(fd);
|
|
7316
|
+
if (!before.isFile())
|
|
7317
|
+
unsafe("the path is not a regular file");
|
|
7318
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
7319
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
7320
|
+
}
|
|
7321
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
7322
|
+
if (uid !== undefined && before.uid !== uid)
|
|
7323
|
+
unsafe("the file is not owned by the current user");
|
|
7324
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
7325
|
+
unsafe("the file exceeds the size limit");
|
|
7326
|
+
const bytes = readFileSync6(fd);
|
|
7327
|
+
const after = fstatSync(fd);
|
|
7328
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
7329
|
+
unsafe("the file changed while being read");
|
|
7330
|
+
}
|
|
7331
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
7332
|
+
} finally {
|
|
7333
|
+
if (fd !== -1)
|
|
7334
|
+
closeSync(fd);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
7338
|
+
const parsed = readAppConfigFile(path);
|
|
7339
|
+
if (!parsed)
|
|
7340
|
+
return null;
|
|
7341
|
+
for (const key of apiKeyKeys) {
|
|
7342
|
+
if (parsed.unusable.has(key)) {
|
|
7343
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7347
|
+
if (new Set(values).size > 1) {
|
|
7348
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
7349
|
+
}
|
|
7350
|
+
return values[0] ?? null;
|
|
7351
|
+
}
|
|
7352
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
7353
|
+
function appConfigDiskValue(name, env, keys) {
|
|
7354
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
7355
|
+
if (wanted.length === 0)
|
|
7356
|
+
return null;
|
|
7357
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
7358
|
+
const parsed = readAppConfigFile(path);
|
|
7359
|
+
if (!parsed)
|
|
7360
|
+
continue;
|
|
7361
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
7362
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
7363
|
+
}
|
|
7364
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7365
|
+
if (new Set(values).size > 1)
|
|
7366
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
7367
|
+
for (const key of wanted) {
|
|
7368
|
+
if (parsed.unusable.has(key))
|
|
7369
|
+
return { key, value: "", path, unusable: true };
|
|
7370
|
+
const value = parsed.values.get(key)?.trim();
|
|
7371
|
+
if (value)
|
|
7372
|
+
return { key, value, path };
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
return null;
|
|
7376
|
+
}
|
|
7377
|
+
function assertUsableCredential(appName, source, value) {
|
|
7378
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
7379
|
+
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]);
|
|
7380
|
+
}
|
|
7381
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
7382
|
+
return;
|
|
7383
|
+
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]);
|
|
7384
|
+
}
|
|
7385
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
7386
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
7387
|
+
function sealCredential(fields) {
|
|
7388
|
+
const { apiKey } = fields;
|
|
7389
|
+
const visible = {
|
|
7390
|
+
tier: fields.tier,
|
|
7391
|
+
source: fields.source,
|
|
7392
|
+
deliberate: fields.deliberate,
|
|
7393
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
7394
|
+
warning: fields.warning
|
|
7395
|
+
};
|
|
7396
|
+
const sealed = { ...visible };
|
|
7397
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
7398
|
+
value: apiKey,
|
|
7399
|
+
enumerable: false,
|
|
7400
|
+
writable: false,
|
|
7401
|
+
configurable: false
|
|
7402
|
+
});
|
|
7403
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
7404
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
7405
|
+
value: fields.pointerVaultKey,
|
|
7406
|
+
enumerable: false,
|
|
7407
|
+
writable: false,
|
|
7408
|
+
configurable: false
|
|
7409
|
+
});
|
|
7410
|
+
}
|
|
7411
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
7412
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
7413
|
+
enumerable: false,
|
|
7414
|
+
writable: false,
|
|
7415
|
+
configurable: false
|
|
7416
|
+
});
|
|
7417
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
7418
|
+
value: true,
|
|
7419
|
+
enumerable: false,
|
|
7420
|
+
writable: false,
|
|
7421
|
+
configurable: false
|
|
7422
|
+
});
|
|
7423
|
+
return Object.freeze(sealed);
|
|
7424
|
+
}
|
|
7425
|
+
function firstEnvValue(env, keys) {
|
|
7426
|
+
for (const key of keys) {
|
|
7427
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
7428
|
+
continue;
|
|
7429
|
+
const value = env[key]?.trim();
|
|
7430
|
+
if (value)
|
|
7431
|
+
return { key, value };
|
|
7432
|
+
}
|
|
7433
|
+
return null;
|
|
7434
|
+
}
|
|
7435
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7436
|
+
function isAmbientEnvironment(env) {
|
|
7437
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
7438
|
+
}
|
|
7439
|
+
function defaultKeychainRunner(argv) {
|
|
7440
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
7441
|
+
encoding: "utf8",
|
|
7442
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7443
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
7444
|
+
});
|
|
7445
|
+
return {
|
|
7446
|
+
status: result.status,
|
|
7447
|
+
stdout: result.stdout ?? "",
|
|
7448
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
7449
|
+
};
|
|
7450
|
+
}
|
|
7451
|
+
function keychainTierEnabled(env, options) {
|
|
7452
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
7453
|
+
return false;
|
|
7454
|
+
if (options.enabled !== undefined)
|
|
7455
|
+
return options.enabled;
|
|
7456
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
7457
|
+
}
|
|
7458
|
+
function keychainAccount(env, options) {
|
|
7459
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
7460
|
+
if (station)
|
|
7461
|
+
return station;
|
|
7462
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
7463
|
+
if (host)
|
|
7464
|
+
return host;
|
|
7465
|
+
const user = env.USER?.trim();
|
|
7466
|
+
return user || null;
|
|
7467
|
+
}
|
|
7468
|
+
function keychainFailureHint(text) {
|
|
7469
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
7470
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
7471
|
+
return clean ? `: ${clean}` : "";
|
|
7472
|
+
}
|
|
7473
|
+
function readKeychainItem(name, env, kind, options) {
|
|
7474
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
7475
|
+
return null;
|
|
7476
|
+
const account = keychainAccount(env, options);
|
|
7477
|
+
if (!account)
|
|
7478
|
+
return null;
|
|
7479
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
7480
|
+
const source = `keychain:${service}@${account}`;
|
|
7481
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
7482
|
+
let result;
|
|
7483
|
+
try {
|
|
7484
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
7485
|
+
} catch (error) {
|
|
7486
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
7487
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
7488
|
+
}
|
|
7489
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
7490
|
+
return null;
|
|
7491
|
+
if (result.status !== 0) {
|
|
7492
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
7493
|
+
}
|
|
7494
|
+
const value = result.stdout.trim();
|
|
7495
|
+
if (!value) {
|
|
7496
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
7497
|
+
}
|
|
7498
|
+
return { value, source };
|
|
7499
|
+
}
|
|
7500
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
7501
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
7502
|
+
}
|
|
7503
|
+
function snapshotClientEnvironment(name, env) {
|
|
7504
|
+
const keys = clientTransportEnvKeys(name);
|
|
7505
|
+
const ambient = isAmbientEnvironment(env);
|
|
7506
|
+
const snapshot = Object.create(null);
|
|
7507
|
+
for (const key of [
|
|
7508
|
+
...keys.apiUrlKeys,
|
|
7509
|
+
...keys.apiKeyKeys,
|
|
7510
|
+
credentialOverrideEnvKey(name),
|
|
7511
|
+
credentialPointerEnvKey(name),
|
|
7512
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
7513
|
+
"HOME",
|
|
7514
|
+
HASNA_HOME_ENV_KEY,
|
|
7515
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
7516
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
7517
|
+
"USER"
|
|
7518
|
+
]) {
|
|
7519
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
7520
|
+
if (!descriptor)
|
|
7521
|
+
continue;
|
|
7522
|
+
if (!("value" in descriptor)) {
|
|
7523
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
7524
|
+
}
|
|
7525
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
7526
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
7527
|
+
}
|
|
7528
|
+
snapshot[key] = descriptor.value;
|
|
7529
|
+
}
|
|
7530
|
+
if (ambient) {
|
|
7531
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
7532
|
+
value: true,
|
|
7533
|
+
enumerable: false,
|
|
7534
|
+
writable: false,
|
|
7535
|
+
configurable: false
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
return Object.freeze(snapshot);
|
|
7539
|
+
}
|
|
7540
|
+
function resolveCredential(name, env, options = {}) {
|
|
7541
|
+
env = snapshotClientEnvironment(name, env);
|
|
7542
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
7543
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
7544
|
+
if (options.apiKey !== undefined) {
|
|
7545
|
+
const explicitKey = options.apiKey.trim();
|
|
7546
|
+
if (!explicitKey) {
|
|
7547
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
7548
|
+
}
|
|
7549
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
7550
|
+
return sealCredential({
|
|
7551
|
+
apiKey: explicitKey,
|
|
7552
|
+
tier: "argument",
|
|
7553
|
+
source: "explicit apiKey argument",
|
|
7554
|
+
deliberate: true,
|
|
7555
|
+
diskCandidates: diskPaths,
|
|
7556
|
+
warning: null
|
|
7557
|
+
});
|
|
7558
|
+
}
|
|
7559
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
7560
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
7561
|
+
if (overrideRaw !== undefined) {
|
|
7562
|
+
const override = overrideRaw.trim();
|
|
7563
|
+
if (!override) {
|
|
7564
|
+
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]);
|
|
7565
|
+
}
|
|
7566
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
7567
|
+
return sealCredential({
|
|
7568
|
+
apiKey: override,
|
|
7569
|
+
tier: "override",
|
|
7570
|
+
source: overrideKeyName,
|
|
7571
|
+
deliberate: true,
|
|
7572
|
+
diskCandidates: diskPaths,
|
|
7573
|
+
warning: null
|
|
7574
|
+
});
|
|
7575
|
+
}
|
|
7576
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
7577
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
7578
|
+
if (pointerRaw !== undefined) {
|
|
7579
|
+
const pointer = pointerRaw.trim();
|
|
7580
|
+
if (!pointer) {
|
|
7581
|
+
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]);
|
|
7582
|
+
}
|
|
7583
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
7584
|
+
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]);
|
|
7585
|
+
}
|
|
7586
|
+
return sealCredential({
|
|
7587
|
+
apiKey: "",
|
|
7588
|
+
pointerVaultKey: pointer,
|
|
7589
|
+
tier: "pointer",
|
|
7590
|
+
source: pointerKeyName,
|
|
7591
|
+
deliberate: true,
|
|
7592
|
+
diskCandidates: diskPaths,
|
|
7593
|
+
warning: null
|
|
7594
|
+
});
|
|
7595
|
+
}
|
|
7596
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
7597
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
7598
|
+
}
|
|
7599
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
7600
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
7601
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
7602
|
+
}
|
|
7603
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
7604
|
+
if (profile) {
|
|
7605
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
7606
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
7607
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
7608
|
+
}
|
|
7609
|
+
const paths = profileDiskSources(name, env, profile);
|
|
7610
|
+
for (const path of paths) {
|
|
7611
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
7612
|
+
if (value) {
|
|
7613
|
+
assertUsableCredential(name, path, value);
|
|
7614
|
+
return sealCredential({
|
|
7615
|
+
apiKey: value,
|
|
7616
|
+
tier: "profile",
|
|
7617
|
+
source: path,
|
|
7618
|
+
deliberate: true,
|
|
7619
|
+
diskCandidates: paths,
|
|
7620
|
+
warning: null
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
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);
|
|
7625
|
+
}
|
|
7626
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
7627
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
7628
|
+
if (blankEnv) {
|
|
7629
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
7630
|
+
}
|
|
7631
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
7632
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
7633
|
+
}
|
|
7634
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
7635
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
7636
|
+
if (keychainHit) {
|
|
7637
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
7638
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain 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;
|
|
7639
|
+
return sealCredential({
|
|
7640
|
+
apiKey: keychainHit.value,
|
|
7641
|
+
tier: "keychain",
|
|
7642
|
+
source: keychainHit.source,
|
|
7643
|
+
deliberate: false,
|
|
7644
|
+
diskCandidates: diskPaths,
|
|
7645
|
+
warning
|
|
7646
|
+
});
|
|
7647
|
+
}
|
|
7648
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
7649
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
7650
|
+
if (diskHits.length > 0) {
|
|
7651
|
+
const winner = diskHits[0];
|
|
7652
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
7653
|
+
const divergentSources = [
|
|
7654
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
7655
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
7656
|
+
];
|
|
7657
|
+
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;
|
|
7658
|
+
return sealCredential({
|
|
7659
|
+
apiKey: winner.value,
|
|
7660
|
+
tier: winner.src.tier,
|
|
7661
|
+
source: winner.src.path,
|
|
7662
|
+
deliberate: false,
|
|
7663
|
+
diskCandidates: diskPaths,
|
|
7664
|
+
warning
|
|
7665
|
+
});
|
|
7666
|
+
}
|
|
7667
|
+
if (envHit) {
|
|
7668
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
7669
|
+
return sealCredential({
|
|
7670
|
+
apiKey: envHit.value,
|
|
7671
|
+
tier: "env",
|
|
7672
|
+
source: envHit.key,
|
|
7673
|
+
deliberate: false,
|
|
7674
|
+
diskCandidates: diskPaths,
|
|
7675
|
+
warning: null
|
|
7676
|
+
});
|
|
7677
|
+
}
|
|
7678
|
+
return null;
|
|
7679
|
+
}
|
|
7680
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
7681
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
7682
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
7683
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
7684
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
7685
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
7686
|
+
}
|
|
7687
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7688
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7689
|
+
function isValidDnsDomain(value) {
|
|
7690
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
7691
|
+
return false;
|
|
7692
|
+
}
|
|
7693
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
7694
|
+
}
|
|
7695
|
+
function validateAppSlug(name) {
|
|
7696
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
7697
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
7698
|
+
}
|
|
7699
|
+
return name;
|
|
7700
|
+
}
|
|
7701
|
+
function rawAuthority(value) {
|
|
7702
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
7703
|
+
if (!match)
|
|
7704
|
+
throw new Error("API URL must be absolute.");
|
|
7705
|
+
const afterScheme = value.slice(match[0].length);
|
|
7706
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
7707
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
7708
|
+
if (!authority)
|
|
7709
|
+
throw new Error("API URL must include a hostname.");
|
|
7710
|
+
return authority;
|
|
7711
|
+
}
|
|
7712
|
+
function assertCanonicalPort(port) {
|
|
7713
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
7714
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7715
|
+
}
|
|
7716
|
+
const numericPort = Number(port);
|
|
7717
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
7718
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
function canonicalAuthorityHostname(authority) {
|
|
7722
|
+
let rawHostname;
|
|
7723
|
+
if (authority.startsWith("[")) {
|
|
7724
|
+
const closingBracket = authority.indexOf("]");
|
|
7725
|
+
if (closingBracket === -1) {
|
|
7726
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
7727
|
+
}
|
|
7728
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
7729
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
7730
|
+
if (portSuffix) {
|
|
7731
|
+
if (!portSuffix.startsWith(":")) {
|
|
7732
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
7733
|
+
}
|
|
7734
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
7735
|
+
}
|
|
7736
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
7737
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
7738
|
+
}
|
|
7739
|
+
} else {
|
|
7740
|
+
const firstColon = authority.indexOf(":");
|
|
7741
|
+
const lastColon = authority.lastIndexOf(":");
|
|
7742
|
+
if (firstColon !== lastColon) {
|
|
7743
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
7744
|
+
}
|
|
7745
|
+
if (lastColon !== -1) {
|
|
7746
|
+
const port = authority.slice(lastColon + 1);
|
|
7747
|
+
assertCanonicalPort(port);
|
|
7748
|
+
rawHostname = authority.slice(0, lastColon);
|
|
7749
|
+
} else {
|
|
7750
|
+
rawHostname = authority;
|
|
7751
|
+
}
|
|
7752
|
+
const ipVersion = isIP(rawHostname);
|
|
7753
|
+
const numericAddressParts = rawHostname.split(".");
|
|
7754
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
7755
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
7756
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
return rawHostname.toLowerCase();
|
|
7760
|
+
}
|
|
7761
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
7762
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
7763
|
+
}
|
|
7764
|
+
function toV1BaseUrl(apiUrl) {
|
|
7765
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
7766
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
7767
|
+
}
|
|
7768
|
+
const input = apiUrl.trim();
|
|
7769
|
+
const authority = rawAuthority(input);
|
|
7770
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
7771
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
7772
|
+
}
|
|
7773
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
7774
|
+
const url = new URL(input);
|
|
7775
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7776
|
+
throw new Error("API URL must use http or https.");
|
|
7777
|
+
}
|
|
7778
|
+
if (url.username || url.password) {
|
|
7779
|
+
throw new Error("API URL must not include credentials.");
|
|
7780
|
+
}
|
|
7781
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
7782
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
7783
|
+
}
|
|
7784
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
7785
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
7786
|
+
}
|
|
7787
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
7788
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
7789
|
+
}
|
|
7790
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
7791
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
7792
|
+
}
|
|
7793
|
+
if (url.search || url.hash) {
|
|
7794
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
7795
|
+
}
|
|
7796
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
7797
|
+
if (path.endsWith("/v1"))
|
|
7798
|
+
path = path.slice(0, -"/v1".length);
|
|
7799
|
+
url.pathname = `${path}/v1`;
|
|
7800
|
+
return url.toString().replace(/\/+$/, "");
|
|
7801
|
+
}
|
|
7802
|
+
class ClientTransportConfigurationError extends Error {
|
|
7803
|
+
appName;
|
|
7804
|
+
sources;
|
|
7805
|
+
constructor(appName, message, sources = []) {
|
|
7806
|
+
super(message);
|
|
7807
|
+
this.name = "ClientTransportConfigurationError";
|
|
7808
|
+
this.appName = appName;
|
|
7809
|
+
this.sources = Object.freeze([...sources]);
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
7813
|
+
env = snapshotClientEnvironment(name, env);
|
|
7814
|
+
const keys = clientTransportEnvKeys(name);
|
|
7815
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
7816
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
7817
|
+
if (blankUrl) {
|
|
7818
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
7819
|
+
}
|
|
7820
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
7821
|
+
if (controlledUrl) {
|
|
7822
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
7823
|
+
}
|
|
7824
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
7825
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
7826
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
7827
|
+
}
|
|
7828
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
7829
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
7830
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
7831
|
+
if (diskConfigUrlHit?.unusable) {
|
|
7832
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
7833
|
+
}
|
|
7834
|
+
const urlCandidates = [
|
|
7835
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
7836
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
7837
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
7838
|
+
];
|
|
7839
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
7840
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
7841
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
7842
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
7843
|
+
}
|
|
7844
|
+
const warnings = [];
|
|
7845
|
+
if (configuredUrl && !envUrlHit) {
|
|
7846
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
7847
|
+
}
|
|
7848
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
7849
|
+
if (!credential) {
|
|
7850
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
7851
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
7852
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
7853
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
7854
|
+
}
|
|
7855
|
+
if (credential.warning)
|
|
7856
|
+
warnings.push(credential.warning);
|
|
7857
|
+
let urlHit;
|
|
7858
|
+
if (configuredUrl) {
|
|
7859
|
+
urlHit = configuredUrl;
|
|
7860
|
+
} else {
|
|
7861
|
+
try {
|
|
7862
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
7863
|
+
} catch (error) {
|
|
7864
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7865
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
const apiUrlSource = urlHit.key;
|
|
7869
|
+
let baseUrl;
|
|
7870
|
+
try {
|
|
7871
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
7872
|
+
} catch (error) {
|
|
7873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7874
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
7875
|
+
}
|
|
7876
|
+
return {
|
|
7877
|
+
resolution: {
|
|
7878
|
+
transport: "http",
|
|
7879
|
+
transportSource: urlHit.key,
|
|
7880
|
+
baseUrl,
|
|
7881
|
+
apiUrlSource,
|
|
7882
|
+
apiKeyPresent: true,
|
|
7883
|
+
apiKeySource: credential.source,
|
|
7884
|
+
apiKeyTier: credential.tier,
|
|
7885
|
+
misconfigured: false,
|
|
7886
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
7887
|
+
},
|
|
7888
|
+
credential
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
7892
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
7893
|
+
}
|
|
7894
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
7895
|
+
const paths = credentialDiskSources(name, env);
|
|
7896
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7897
|
+
}
|
|
7898
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7899
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
7900
|
+
"host",
|
|
7901
|
+
":authority",
|
|
7902
|
+
"forwarded",
|
|
7903
|
+
"x-forwarded-host",
|
|
7904
|
+
"x-original-host"
|
|
7905
|
+
]);
|
|
7906
|
+
|
|
7907
|
+
// src/lib/transport.ts
|
|
7908
|
+
init_config();
|
|
7909
|
+
|
|
7910
|
+
// src/lib/local-opt-in.ts
|
|
7911
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
7912
|
+
function isHooksLocalOptIn(env = process.env) {
|
|
7913
|
+
return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
7914
|
+
}
|
|
7915
|
+
function hooksAuthorityEnvKeys() {
|
|
7916
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
7917
|
+
return [
|
|
7918
|
+
...keys.apiUrlKeys,
|
|
7919
|
+
...keys.apiKeyKeys,
|
|
7920
|
+
credentialOverrideEnvKey("hooks"),
|
|
7921
|
+
credentialPointerEnvKey("hooks"),
|
|
7922
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
7923
|
+
];
|
|
7924
|
+
}
|
|
7925
|
+
function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
7926
|
+
return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
7927
|
+
}
|
|
7928
|
+
function selectsHooksLocalStore(env = process.env) {
|
|
7929
|
+
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
7930
|
+
}
|
|
7931
|
+
function hooksResolverEnv(env) {
|
|
7932
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
7933
|
+
if (blanks.length === 0)
|
|
7934
|
+
return env;
|
|
7935
|
+
const next = { ...env };
|
|
7936
|
+
for (const key of blanks)
|
|
7937
|
+
delete next[key];
|
|
7938
|
+
return next;
|
|
7939
|
+
}
|
|
7940
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7941
|
+
function isAmbientHooksEnv(env) {
|
|
7942
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
7943
|
+
return true;
|
|
7944
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
7945
|
+
}
|
|
7946
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
7947
|
+
const normalised = hooksResolverEnv(env);
|
|
7948
|
+
if (normalised === env)
|
|
7949
|
+
return { env: normalised, credentials };
|
|
7950
|
+
const keychain = { ...credentials.keychain };
|
|
7951
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
7952
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
7953
|
+
}
|
|
7954
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7957
|
+
// src/lib/transport.ts
|
|
7958
|
+
var localNoticePrinted = false;
|
|
7959
|
+
function announceLocal(notice, reason) {
|
|
7960
|
+
if (localNoticePrinted)
|
|
7961
|
+
return;
|
|
7962
|
+
localNoticePrinted = true;
|
|
7963
|
+
const line = `hooks: LOCAL mode \u2014 ${reason}; using the bundled registry and the local store at ` + `${getHooksDataDir()}, not a remote registry. Set HASNA_HOOKS_API_KEY (or the Keychain item ` + `hasna.credentials.hooks.api-key / ~/.hasna/hooks/config/credentials) to go remote.`;
|
|
7964
|
+
if (notice)
|
|
7965
|
+
notice(line);
|
|
7966
|
+
else if (typeof process !== "undefined")
|
|
7967
|
+
process.stderr.write(`${line}
|
|
7968
|
+
`);
|
|
7969
|
+
}
|
|
7970
|
+
function hooksRegistryOrigin(v1BaseUrl) {
|
|
7971
|
+
return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
7972
|
+
}
|
|
7973
|
+
function rethrowHooksAuthorityFailure(error) {
|
|
7974
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7975
|
+
const name = error instanceof Error ? error.name : "";
|
|
7976
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
7977
|
+
throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
7978
|
+
}
|
|
7979
|
+
if (/no API key could be resolved/.test(message)) {
|
|
7980
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
7981
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved from the Keychain item " + `hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1, alias HOOKS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
|
|
7982
|
+
}
|
|
7983
|
+
throw new Error("REMOTE_API_KEY_MISSING: the remote hooks registry requires HASNA_HOOKS_API_KEY, the Keychain item " + `hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
7984
|
+
}
|
|
7985
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
7986
|
+
}
|
|
7987
|
+
function resolveHooksTransport(env = process.env, options = {}) {
|
|
7988
|
+
if (selectsHooksLocalStore(env)) {
|
|
7989
|
+
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
7990
|
+
return { mode: "local", source: "local-opt-in", authority: null };
|
|
7991
|
+
}
|
|
7992
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
7993
|
+
const tier1 = options.credentials?.apiKey;
|
|
7994
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
7995
|
+
let resolution;
|
|
7996
|
+
let credential;
|
|
7997
|
+
try {
|
|
7998
|
+
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
7999
|
+
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
8000
|
+
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
8001
|
+
});
|
|
8002
|
+
} catch (error) {
|
|
8003
|
+
rethrowHooksAuthorityFailure(error);
|
|
8004
|
+
}
|
|
8005
|
+
if (!credential) {
|
|
8006
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
8007
|
+
}
|
|
8008
|
+
return {
|
|
8009
|
+
mode: "remote",
|
|
8010
|
+
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
8011
|
+
authority: {
|
|
8012
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
8013
|
+
apiKey: credential.apiKey,
|
|
8014
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
8015
|
+
apiKeySource: credential.source,
|
|
8016
|
+
apiKeyTier: credential.tier,
|
|
8017
|
+
v1BaseUrl: resolution.baseUrl,
|
|
8018
|
+
warning: resolution.warning
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
}
|
|
8022
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
8023
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8024
|
+
const tier1 = options.credentials?.apiKey;
|
|
8025
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
8026
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
// src/index.ts
|
|
7543
8030
|
init_store();
|
|
7544
8031
|
init_manifest();
|
|
7545
|
-
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
7546
8032
|
|
|
7547
8033
|
// src/lib/custom-install.ts
|
|
7548
8034
|
init_config();
|
|
7549
8035
|
init_manifest();
|
|
7550
|
-
import { existsSync as
|
|
7551
|
-
import { join as
|
|
8036
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
|
|
8037
|
+
import { join as join10 } from "path";
|
|
7552
8038
|
import { tmpdir } from "os";
|
|
7553
8039
|
function manifestAt(dir) {
|
|
7554
|
-
const path =
|
|
7555
|
-
if (!
|
|
8040
|
+
const path = join10(dir, "manifest.json");
|
|
8041
|
+
if (!existsSync9(path))
|
|
7556
8042
|
return;
|
|
7557
8043
|
try {
|
|
7558
|
-
return parseManifest(
|
|
8044
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
7559
8045
|
} catch {
|
|
7560
8046
|
return;
|
|
7561
8047
|
}
|
|
@@ -7574,8 +8060,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7574
8060
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7575
8061
|
}
|
|
7576
8062
|
async function cloneToTemp(gitUrl) {
|
|
7577
|
-
const tmp = mkdtempSync(
|
|
7578
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8063
|
+
const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
|
|
8064
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
|
|
7579
8065
|
stdout: "pipe",
|
|
7580
8066
|
stderr: "pipe"
|
|
7581
8067
|
});
|
|
@@ -7585,10 +8071,10 @@ async function cloneToTemp(gitUrl) {
|
|
|
7585
8071
|
proc.exited
|
|
7586
8072
|
]);
|
|
7587
8073
|
if (exitCode !== 0) {
|
|
7588
|
-
|
|
8074
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
7589
8075
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7590
8076
|
}
|
|
7591
|
-
return
|
|
8077
|
+
return join10(tmp, "repo");
|
|
7592
8078
|
}
|
|
7593
8079
|
function isGitUrl(value) {
|
|
7594
8080
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7598,7 +8084,7 @@ function isCustomSource(value) {
|
|
|
7598
8084
|
return true;
|
|
7599
8085
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7600
8086
|
return true;
|
|
7601
|
-
return
|
|
8087
|
+
return existsSync9(value);
|
|
7602
8088
|
}
|
|
7603
8089
|
async function installCustomSource(source) {
|
|
7604
8090
|
if (isGitUrl(source)) {
|
|
@@ -7606,17 +8092,17 @@ async function installCustomSource(source) {
|
|
|
7606
8092
|
try {
|
|
7607
8093
|
return copyManifestDir(repoDir, source, "git");
|
|
7608
8094
|
} finally {
|
|
7609
|
-
|
|
8095
|
+
rmSync2(repoDir, { recursive: true, force: true });
|
|
7610
8096
|
}
|
|
7611
8097
|
}
|
|
7612
8098
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7613
8099
|
return installManifestUrl(source);
|
|
7614
8100
|
}
|
|
7615
|
-
if (!
|
|
8101
|
+
if (!existsSync9(source)) {
|
|
7616
8102
|
throw new Error(`No such local path: ${source}`);
|
|
7617
8103
|
}
|
|
7618
8104
|
if (source.endsWith("manifest.json")) {
|
|
7619
|
-
const manifest = parseManifest(
|
|
8105
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
7620
8106
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7621
8107
|
}
|
|
7622
8108
|
return copyManifestDir(source, source, "local");
|
|
@@ -7647,8 +8133,7 @@ async function installManifestUrl(url) {
|
|
|
7647
8133
|
}
|
|
7648
8134
|
// src/lib/sync.ts
|
|
7649
8135
|
import { createHash as createHash2 } from "crypto";
|
|
7650
|
-
import { readFileSync as
|
|
7651
|
-
init_config();
|
|
8136
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
7652
8137
|
init_db();
|
|
7653
8138
|
init_store();
|
|
7654
8139
|
init_manifest();
|
|
@@ -7656,7 +8141,7 @@ function sha256OfText(text) {
|
|
|
7656
8141
|
return createHash2("sha256").update(text).digest("hex");
|
|
7657
8142
|
}
|
|
7658
8143
|
function sha256OfPath(path) {
|
|
7659
|
-
return createHash2("sha256").update(
|
|
8144
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
7660
8145
|
}
|
|
7661
8146
|
function collectBundledCatalog() {
|
|
7662
8147
|
const out = [];
|
|
@@ -7672,9 +8157,8 @@ function collectBundledCatalog() {
|
|
|
7672
8157
|
}
|
|
7673
8158
|
return out;
|
|
7674
8159
|
}
|
|
7675
|
-
async function fetchJson(base, path, opts) {
|
|
8160
|
+
async function fetchJson(base, path, apiKey, opts) {
|
|
7676
8161
|
const headers = { accept: "application/json" };
|
|
7677
|
-
const apiKey = resolveApiKey();
|
|
7678
8162
|
if (apiKey)
|
|
7679
8163
|
headers["x-api-key"] = apiKey;
|
|
7680
8164
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -7683,7 +8167,7 @@ async function fetchJson(base, path, opts) {
|
|
|
7683
8167
|
signal: AbortSignal.timeout(15000)
|
|
7684
8168
|
});
|
|
7685
8169
|
if (res.status === 401) {
|
|
7686
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or
|
|
8170
|
+
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, " + "or ~/.hasna/hooks/config/credentials (resolved per call, never stored)");
|
|
7687
8171
|
}
|
|
7688
8172
|
if (!res.ok) {
|
|
7689
8173
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
@@ -7694,9 +8178,9 @@ async function fetchJson(base, path, opts) {
|
|
|
7694
8178
|
}
|
|
7695
8179
|
return body;
|
|
7696
8180
|
}
|
|
7697
|
-
async function fetchRemoteState(apiUrl) {
|
|
7698
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
7699
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8181
|
+
async function fetchRemoteState(apiUrl, apiKey) {
|
|
8182
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
|
|
8183
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7700
8184
|
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
7701
8185
|
throw new Error("remote catalog is malformed: missing hooks array");
|
|
7702
8186
|
}
|
|
@@ -7739,20 +8223,25 @@ function computeDiff(catalog, remoteLock) {
|
|
|
7739
8223
|
return diff;
|
|
7740
8224
|
}
|
|
7741
8225
|
async function planSync(options = {}) {
|
|
7742
|
-
const
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
8226
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8227
|
+
credentials: options.credentials
|
|
8228
|
+
});
|
|
8229
|
+
if (transport.mode === "remote" && transport.authority) {
|
|
8230
|
+
const { origin, apiKey } = transport.authority;
|
|
8231
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
8232
|
+
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
7746
8233
|
}
|
|
7747
8234
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
7748
8235
|
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
7749
8236
|
}
|
|
7750
8237
|
async function syncHooks(options = {}) {
|
|
7751
|
-
const
|
|
7752
|
-
|
|
8238
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8239
|
+
credentials: options.credentials
|
|
8240
|
+
});
|
|
8241
|
+
const plan = await planSync(options);
|
|
7753
8242
|
if (options.dryRun)
|
|
7754
8243
|
return { ...plan, dryRun: true };
|
|
7755
|
-
if (!
|
|
8244
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
7756
8245
|
const db = getDb();
|
|
7757
8246
|
for (const entry of collectBundledCatalog()) {
|
|
7758
8247
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
@@ -7767,19 +8256,20 @@ async function syncHooks(options = {}) {
|
|
|
7767
8256
|
}
|
|
7768
8257
|
return plan;
|
|
7769
8258
|
}
|
|
7770
|
-
const {
|
|
8259
|
+
const { origin, apiKey } = transport.authority;
|
|
8260
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
7771
8261
|
const diff = computeDiff(catalog, remoteLock);
|
|
7772
|
-
const staged = await stageSyncArtifacts(
|
|
7773
|
-
await commitSyncArtifacts(staged,
|
|
7774
|
-
return { apiUrl, dryRun: false, diff };
|
|
8262
|
+
const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
|
|
8263
|
+
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
8264
|
+
return { apiUrl: origin, dryRun: false, diff };
|
|
7775
8265
|
}
|
|
7776
|
-
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
8266
|
+
async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
7777
8267
|
const staged = [];
|
|
7778
8268
|
for (const name of names) {
|
|
7779
8269
|
const entry = remoteLock.hooks[name];
|
|
7780
8270
|
if (!entry)
|
|
7781
8271
|
continue;
|
|
7782
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}
|
|
8272
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
|
|
7783
8273
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7784
8274
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
7785
8275
|
}
|
|
@@ -7846,8 +8336,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7846
8336
|
throw err;
|
|
7847
8337
|
}
|
|
7848
8338
|
}
|
|
7849
|
-
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7850
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8339
|
+
async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
8340
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7851
8341
|
const entry = remoteLock.hooks?.[name];
|
|
7852
8342
|
if (!entry) {
|
|
7853
8343
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
@@ -7856,7 +8346,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7856
8346
|
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7857
8347
|
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
7858
8348
|
}
|
|
7859
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
8349
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
|
|
7860
8350
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7861
8351
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7862
8352
|
}
|
|
@@ -7910,9 +8400,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7910
8400
|
// src/serve.ts
|
|
7911
8401
|
init_manifest();
|
|
7912
8402
|
init_store();
|
|
7913
|
-
|
|
7914
|
-
import {
|
|
7915
|
-
import { dirname as dirname4, join as join12 } from "path";
|
|
8403
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8404
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
7916
8405
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7917
8406
|
|
|
7918
8407
|
// src/lib/secure-compare.ts
|
|
@@ -8073,7 +8562,7 @@ var openApiDocument = {
|
|
|
8073
8562
|
apiKey: {
|
|
8074
8563
|
type: "http",
|
|
8075
8564
|
scheme: "bearer",
|
|
8076
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
8565
|
+
description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
|
|
8077
8566
|
}
|
|
8078
8567
|
}
|
|
8079
8568
|
}
|
|
@@ -8086,8 +8575,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
8086
8575
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
8087
8576
|
function packageVersion() {
|
|
8088
8577
|
try {
|
|
8089
|
-
const pkgPath =
|
|
8090
|
-
return JSON.parse(
|
|
8578
|
+
const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8579
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
|
|
8091
8580
|
} catch {
|
|
8092
8581
|
return "0.0.0";
|
|
8093
8582
|
}
|
|
@@ -8244,12 +8733,11 @@ function resolveServeOptions(options) {
|
|
|
8244
8733
|
}
|
|
8245
8734
|
function startServeServer(options) {
|
|
8246
8735
|
const { port, host } = resolveServeOptions(options);
|
|
8247
|
-
const apiKey = resolveApiKey();
|
|
8248
8736
|
const server = Bun.serve({
|
|
8249
8737
|
hostname: host,
|
|
8250
8738
|
port,
|
|
8251
8739
|
async fetch(req) {
|
|
8252
|
-
return handleServeRequest(req,
|
|
8740
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8253
8741
|
}
|
|
8254
8742
|
});
|
|
8255
8743
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
@@ -8354,11 +8842,11 @@ async function runHook(name, input, options = {}) {
|
|
|
8354
8842
|
throw new Error(`Hook '${name}' not found`);
|
|
8355
8843
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
8356
8844
|
const resolvedScript = custom2?.scriptPath;
|
|
8357
|
-
const script = resolvedScript ?? (
|
|
8845
|
+
const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
|
|
8358
8846
|
if (!script)
|
|
8359
8847
|
throw new Error(`Hook script not found: ${name}`);
|
|
8360
8848
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
8361
|
-
const content =
|
|
8849
|
+
const content = readFileSync10(script);
|
|
8362
8850
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
8363
8851
|
if (!check.ok) {
|
|
8364
8852
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|
|
@@ -8449,7 +8937,6 @@ async function runHook(name, input, options = {}) {
|
|
|
8449
8937
|
export {
|
|
8450
8938
|
writeLock,
|
|
8451
8939
|
writeCustomHook,
|
|
8452
|
-
writeConfig,
|
|
8453
8940
|
verifyScriptHash,
|
|
8454
8941
|
upsertHookRecord,
|
|
8455
8942
|
updateProfile,
|
|
@@ -8470,11 +8957,11 @@ export {
|
|
|
8470
8957
|
resolveTables,
|
|
8471
8958
|
resolveScriptPath,
|
|
8472
8959
|
resolveScript,
|
|
8960
|
+
resolveHooksTransport,
|
|
8961
|
+
resolveHooksServePublishKey,
|
|
8473
8962
|
resolveHookMeta,
|
|
8474
8963
|
resolveHookDir,
|
|
8475
8964
|
resolveHook,
|
|
8476
|
-
resolveApiUrl,
|
|
8477
|
-
resolveApiKey,
|
|
8478
8965
|
removeProjectHook,
|
|
8479
8966
|
removePinnedHook,
|
|
8480
8967
|
removeHookRecord,
|
|
@@ -8482,7 +8969,6 @@ export {
|
|
|
8482
8969
|
removeHook,
|
|
8483
8970
|
readLock,
|
|
8484
8971
|
readCustomManifest,
|
|
8485
|
-
readConfig,
|
|
8486
8972
|
provisionCloudflareResources,
|
|
8487
8973
|
planSync,
|
|
8488
8974
|
pinInstalledHook,
|
|
@@ -8500,6 +8986,7 @@ export {
|
|
|
8500
8986
|
installHook,
|
|
8501
8987
|
installCustomSource,
|
|
8502
8988
|
importProfiles,
|
|
8989
|
+
hooksRegistryOrigin,
|
|
8503
8990
|
hookExists,
|
|
8504
8991
|
handleServeRequest,
|
|
8505
8992
|
getSyncMetaAll,
|
|
@@ -8524,7 +9011,6 @@ export {
|
|
|
8524
9011
|
getHookEvents,
|
|
8525
9012
|
getHook,
|
|
8526
9013
|
getCustomHooksDir,
|
|
8527
|
-
getConfigPath,
|
|
8528
9014
|
fetchPinnedHook,
|
|
8529
9015
|
exportProfiles,
|
|
8530
9016
|
deleteProfile,
|