@hasna/hooks 0.7.10 → 0.9.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 +2640 -1820
- package/bin/serve.js +742 -551
- package/dist/config.d.ts +25 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1251 -717
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/installer.d.ts +40 -10
- package/dist/lib/local-opt-in.d.ts +80 -0
- package/dist/lib/profiles.d.ts +10 -0
- package/dist/lib/registration.d.ts +75 -0
- package/dist/lib/registry.d.ts +14 -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/README.md +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-trash-guard/README.md +132 -0
- package/hooks/hook-trash-guard/package.json +12 -0
- package/hooks/hook-trash-guard/src/hook.ts +1114 -0
- package/hooks/hook-typecheck-gate/package.json +1 -1
- package/package.json +7 -10
- package/scripts/ensure-profiles-dir.mjs +52 -1
- package/hooks/codewith-native-common.test.ts +0 -1935
- package/hooks/hook-affected-tests/tsconfig.json +0 -25
- package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
- package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
- package/hooks/hook-announce-start/tsconfig.json +0 -25
- package/hooks/hook-announce-stop/tsconfig.json +0 -25
- package/hooks/hook-autoformat/tsconfig.json +0 -25
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-conflict-detect/tsconfig.json +0 -25
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-desktopnotify/tsconfig.json +0 -25
- package/hooks/hook-dm-inject/tsconfig.json +0 -25
- package/hooks/hook-envsetup/tsconfig.json +0 -25
- package/hooks/hook-failure-to-task/tsconfig.json +0 -25
- package/hooks/hook-filelock/tsconfig.json +0 -25
- package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
- package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
- package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
- package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-permissionguard/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/hooks/hook-protectfiles/tsconfig.json +0 -25
- package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
- package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
- package/hooks/hook-stylescheck/tsconfig.json +0 -25
- package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
- package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
- package/hooks/mention-context/src/hook.test.ts +0 -68
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
|
}
|
|
@@ -6395,6 +5998,18 @@ var HOOKS = [
|
|
|
6395
5998
|
matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
6396
5999
|
tags: ["workspace", "repos", "structure", "guard", "safety", "orgs", "multi-agent"]
|
|
6397
6000
|
},
|
|
6001
|
+
{
|
|
6002
|
+
name: "trash-guard",
|
|
6003
|
+
displayName: "Trash Guard",
|
|
6004
|
+
description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
|
|
6005
|
+
version: "0.1.0",
|
|
6006
|
+
category: "Git Safety",
|
|
6007
|
+
event: "PreToolUse",
|
|
6008
|
+
matcher: "Bash",
|
|
6009
|
+
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
6010
|
+
rewritesInput: true,
|
|
6011
|
+
timeoutSeconds: 5
|
|
6012
|
+
},
|
|
6398
6013
|
{
|
|
6399
6014
|
name: "checktests",
|
|
6400
6015
|
displayName: "Check Tests",
|
|
@@ -6871,21 +6486,21 @@ function getHook(name) {
|
|
|
6871
6486
|
return HOOKS.find((h) => h.name === name);
|
|
6872
6487
|
}
|
|
6873
6488
|
// src/lib/installer.ts
|
|
6874
|
-
import { existsSync as
|
|
6875
|
-
import { join as
|
|
6876
|
-
import { homedir as
|
|
6489
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
6490
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
6491
|
+
import { homedir as homedir3 } from "os";
|
|
6877
6492
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6878
6493
|
|
|
6879
6494
|
// src/lib/resolve.ts
|
|
6880
|
-
import { dirname as dirname2, join as
|
|
6881
|
-
import { existsSync as
|
|
6495
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
6496
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6882
6497
|
import { fileURLToPath } from "url";
|
|
6883
6498
|
init_manifest();
|
|
6884
6499
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
6885
|
-
const sourceLayout =
|
|
6886
|
-
if (
|
|
6500
|
+
const sourceLayout = join6(moduleDir, "..", "..", "hooks");
|
|
6501
|
+
if (existsSync5(join6(sourceLayout, "hook-gitguard")))
|
|
6887
6502
|
return sourceLayout;
|
|
6888
|
-
return
|
|
6503
|
+
return join6(moduleDir, "..", "hooks");
|
|
6889
6504
|
}
|
|
6890
6505
|
function resolveHookDir(name) {
|
|
6891
6506
|
const custom2 = readCustomManifest(name);
|
|
@@ -6896,11 +6511,11 @@ function resolveHookDir(name) {
|
|
|
6896
6511
|
return;
|
|
6897
6512
|
const short = shortManifestName(name);
|
|
6898
6513
|
const bundledBase = resolveBundledHooksDir();
|
|
6899
|
-
const direct =
|
|
6900
|
-
if (
|
|
6514
|
+
const direct = join6(bundledBase, short);
|
|
6515
|
+
if (existsSync5(direct))
|
|
6901
6516
|
return direct;
|
|
6902
|
-
const prefixed =
|
|
6903
|
-
return
|
|
6517
|
+
const prefixed = join6(bundledBase, `hook-${short}`);
|
|
6518
|
+
return existsSync5(prefixed) ? prefixed : undefined;
|
|
6904
6519
|
}
|
|
6905
6520
|
function resolveScriptPath(name) {
|
|
6906
6521
|
const custom2 = readCustomManifest(name);
|
|
@@ -6909,8 +6524,8 @@ function resolveScriptPath(name) {
|
|
|
6909
6524
|
const dir = resolveHookDir(name);
|
|
6910
6525
|
if (!dir)
|
|
6911
6526
|
return;
|
|
6912
|
-
const script =
|
|
6913
|
-
return
|
|
6527
|
+
const script = join6(dir, "src", "hook.ts");
|
|
6528
|
+
return existsSync5(script) ? script : undefined;
|
|
6914
6529
|
}
|
|
6915
6530
|
function resolveHookMeta(name) {
|
|
6916
6531
|
const custom2 = readCustomManifest(name);
|
|
@@ -6963,10 +6578,17 @@ function resolveHook(name) {
|
|
|
6963
6578
|
|
|
6964
6579
|
// src/lib/installer.ts
|
|
6965
6580
|
init_manifest();
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6581
|
+
|
|
6582
|
+
// src/lib/registration.ts
|
|
6583
|
+
function matchersOverlap(a, b) {
|
|
6584
|
+
return a === b || a.includes(b) || b.includes(a);
|
|
6585
|
+
}
|
|
6586
|
+
|
|
6587
|
+
// src/lib/installer.ts
|
|
6588
|
+
init_store();
|
|
6589
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
6590
|
+
var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
|
|
6591
|
+
function normalizeHookName(name) {
|
|
6970
6592
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
6971
6593
|
}
|
|
6972
6594
|
function shortHookName(name) {
|
|
@@ -7035,25 +6657,25 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
7035
6657
|
return globalOverride;
|
|
7036
6658
|
const dir = getTargetSettingsDir(target);
|
|
7037
6659
|
if (scope === "project") {
|
|
7038
|
-
return target === "codewith" ?
|
|
6660
|
+
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
7039
6661
|
}
|
|
7040
|
-
return target === "codewith" ?
|
|
6662
|
+
return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
|
|
7041
6663
|
}
|
|
7042
6664
|
function getHookPath(name) {
|
|
7043
6665
|
const shortName = shortHookName(name);
|
|
7044
6666
|
const resolved = resolveHookDir(shortName);
|
|
7045
6667
|
if (resolved)
|
|
7046
6668
|
return resolved;
|
|
7047
|
-
return
|
|
6669
|
+
return join7(HOOKS_DIR, normalizeHookName(shortName));
|
|
7048
6670
|
}
|
|
7049
6671
|
function hookExists(name) {
|
|
7050
|
-
return
|
|
6672
|
+
return existsSync7(getHookPath(name));
|
|
7051
6673
|
}
|
|
7052
6674
|
function readSettings(scope = "global", target = "claude") {
|
|
7053
6675
|
const path = getSettingsPath(scope, target);
|
|
7054
6676
|
try {
|
|
7055
|
-
if (
|
|
7056
|
-
return JSON.parse(
|
|
6677
|
+
if (existsSync7(path)) {
|
|
6678
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
7057
6679
|
}
|
|
7058
6680
|
} catch (error) {
|
|
7059
6681
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -7063,10 +6685,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
7063
6685
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
7064
6686
|
const path = getSettingsPath(scope, target);
|
|
7065
6687
|
const dir = dirname3(path);
|
|
7066
|
-
if (!
|
|
7067
|
-
|
|
6688
|
+
if (!existsSync7(dir)) {
|
|
6689
|
+
mkdirSync4(dir, { recursive: true });
|
|
7068
6690
|
}
|
|
7069
|
-
|
|
6691
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
7070
6692
|
`);
|
|
7071
6693
|
}
|
|
7072
6694
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -7098,6 +6720,7 @@ function codewithTimeout(name) {
|
|
|
7098
6720
|
case "prompt-guard":
|
|
7099
6721
|
return 3;
|
|
7100
6722
|
case "worktree-guard":
|
|
6723
|
+
case "trash-guard":
|
|
7101
6724
|
case "stop-sync":
|
|
7102
6725
|
return 5;
|
|
7103
6726
|
default:
|
|
@@ -7116,6 +6739,8 @@ function codewithStatusMessage(name) {
|
|
|
7116
6739
|
return "Checking prompt safety";
|
|
7117
6740
|
case "worktree-guard":
|
|
7118
6741
|
return "Checking worktree safety";
|
|
6742
|
+
case "trash-guard":
|
|
6743
|
+
return "Redirecting rm into trash";
|
|
7119
6744
|
case "stop-sync":
|
|
7120
6745
|
return "Syncing turn-end heartbeat";
|
|
7121
6746
|
default:
|
|
@@ -7152,7 +6777,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
7152
6777
|
function readCodewithConfig(scope, configPath) {
|
|
7153
6778
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7154
6779
|
try {
|
|
7155
|
-
return
|
|
6780
|
+
return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
7156
6781
|
} catch {
|
|
7157
6782
|
return "";
|
|
7158
6783
|
}
|
|
@@ -7160,13 +6785,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
7160
6785
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
7161
6786
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7162
6787
|
const dir = dirname3(path);
|
|
7163
|
-
if (!
|
|
7164
|
-
|
|
7165
|
-
const existing =
|
|
6788
|
+
if (!existsSync7(dir))
|
|
6789
|
+
mkdirSync4(dir, { recursive: true });
|
|
6790
|
+
const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
7166
6791
|
const sep2 = existing.trim() ? `
|
|
7167
6792
|
|
|
7168
6793
|
` : "";
|
|
7169
|
-
|
|
6794
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
7170
6795
|
return path;
|
|
7171
6796
|
}
|
|
7172
6797
|
function detectConflict(name, scope, target) {
|
|
@@ -7183,14 +6808,32 @@ function detectConflict(name, scope, target) {
|
|
|
7183
6808
|
continue;
|
|
7184
6809
|
if (!getHookEvents(existing).some((event) => events.has(event)))
|
|
7185
6810
|
continue;
|
|
7186
|
-
|
|
7187
|
-
const b = existing.matcher.toLowerCase();
|
|
7188
|
-
if (a === b || a.includes(b) || b.includes(a)) {
|
|
6811
|
+
if (matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
7189
6812
|
return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
|
|
7190
6813
|
}
|
|
7191
6814
|
}
|
|
7192
6815
|
return;
|
|
7193
6816
|
}
|
|
6817
|
+
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
6818
|
+
const meta = resolveHookMeta(name);
|
|
6819
|
+
if (!meta?.matcher || !meta.rewritesInput)
|
|
6820
|
+
return;
|
|
6821
|
+
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
6822
|
+
return;
|
|
6823
|
+
for (const existingName of getRegisteredHooksForTarget(scope, target)) {
|
|
6824
|
+
if (existingName === name)
|
|
6825
|
+
continue;
|
|
6826
|
+
const existing = lookup(existingName);
|
|
6827
|
+
if (!existing?.matcher || !existing.rewritesInput)
|
|
6828
|
+
continue;
|
|
6829
|
+
if (!getHookEvents(existing).includes("PreToolUse"))
|
|
6830
|
+
continue;
|
|
6831
|
+
if (!matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase()))
|
|
6832
|
+
continue;
|
|
6833
|
+
return `both '${name}' and '${existingName}' rewrite the tool input on overlapping PreToolUse matchers ('${meta.matcher}' / '${existing.matcher}'). Only one rewrite per tool call is applied, so one guard would silently disappear \u2014 install one of them, or narrow a matcher so they no longer overlap.`;
|
|
6834
|
+
}
|
|
6835
|
+
return;
|
|
6836
|
+
}
|
|
7194
6837
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
|
|
7195
6838
|
const shortName = shortHookName(name);
|
|
7196
6839
|
if (!hookExists(shortName)) {
|
|
@@ -7250,6 +6893,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
7250
6893
|
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
7251
6894
|
}
|
|
7252
6895
|
const conflict = detectConflict(shortName, scope, target);
|
|
6896
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
6897
|
+
if (rewriteConflict) {
|
|
6898
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
6899
|
+
}
|
|
7253
6900
|
try {
|
|
7254
6901
|
registerHook(shortName, scope, target, profile);
|
|
7255
6902
|
return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
|
|
@@ -7321,8 +6968,12 @@ function registerHook(name, scope = "global", target = "claude", profile) {
|
|
|
7321
6968
|
for (const eventKey of uniqueEventKeys) {
|
|
7322
6969
|
if (!settings.hooks[eventKey])
|
|
7323
6970
|
settings.hooks[eventKey] = [];
|
|
6971
|
+
const hookEntry = { type: "command", command: hookCommand };
|
|
6972
|
+
if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0) {
|
|
6973
|
+
hookEntry.timeout = meta.timeoutSeconds;
|
|
6974
|
+
}
|
|
7324
6975
|
const entry = {
|
|
7325
|
-
hooks: [
|
|
6976
|
+
hooks: [hookEntry]
|
|
7326
6977
|
};
|
|
7327
6978
|
if (meta.matcher) {
|
|
7328
6979
|
entry.matcher = meta.matcher;
|
|
@@ -7463,14 +7114,28 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
7463
7114
|
}
|
|
7464
7115
|
function codewithHasHookEntry(name, scope = "global") {
|
|
7465
7116
|
const path = getSettingsPath(scope, "codewith");
|
|
7466
|
-
if (!
|
|
7117
|
+
if (!existsSync7(path))
|
|
7467
7118
|
return false;
|
|
7468
7119
|
try {
|
|
7469
|
-
return codewithHasHookEntryInText(
|
|
7120
|
+
return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
|
|
7470
7121
|
} catch {
|
|
7471
7122
|
return false;
|
|
7472
7123
|
}
|
|
7473
7124
|
}
|
|
7125
|
+
function removeStoreDirFailClosed(dir) {
|
|
7126
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
7127
|
+
for (const entry of entries) {
|
|
7128
|
+
if (entry.isDirectory()) {
|
|
7129
|
+
removeStoreDirFailClosed(join7(dir, entry.name));
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
for (const entry of entries) {
|
|
7133
|
+
if (!entry.isDirectory()) {
|
|
7134
|
+
unlinkSync(join7(dir, entry.name));
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
rmdirSync(dir);
|
|
7138
|
+
}
|
|
7474
7139
|
function uninstallHook(name, scope = "global", target = "claude") {
|
|
7475
7140
|
const shortName = shortHookName(name);
|
|
7476
7141
|
const custom2 = readCustomManifest(shortName);
|
|
@@ -7497,11 +7162,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7497
7162
|
const registrationsRemaining = [];
|
|
7498
7163
|
if (target === "codewith" || target === "all") {
|
|
7499
7164
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
7500
|
-
if (
|
|
7501
|
-
const before =
|
|
7165
|
+
if (existsSync7(codewithPath)) {
|
|
7166
|
+
const before = readFileSync4(codewithPath, "utf-8");
|
|
7502
7167
|
const after = removeCodewithHookEntry(before, shortName);
|
|
7503
7168
|
if (after.removed) {
|
|
7504
|
-
|
|
7169
|
+
writeFileSync3(codewithPath, after.text, "utf-8");
|
|
7505
7170
|
}
|
|
7506
7171
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
7507
7172
|
registrationsRemaining.push("codewith");
|
|
@@ -7510,7 +7175,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7510
7175
|
}
|
|
7511
7176
|
if (custom2) {
|
|
7512
7177
|
try {
|
|
7513
|
-
|
|
7178
|
+
removeStoreDirFailClosed(customHookDir(shortName));
|
|
7514
7179
|
} catch {
|
|
7515
7180
|
return {
|
|
7516
7181
|
name: shortName,
|
|
@@ -7540,22 +7205,891 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7540
7205
|
// src/index.ts
|
|
7541
7206
|
init_profiles();
|
|
7542
7207
|
init_config();
|
|
7208
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
7209
|
+
|
|
7210
|
+
// ../contracts/dist/client/transport.js
|
|
7211
|
+
import { isIP } from "net";
|
|
7212
|
+
import { spawnSync } from "child_process";
|
|
7213
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
7214
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
7215
|
+
import { createRequire } from "module";
|
|
7216
|
+
import { hostname as osHostname } from "os";
|
|
7217
|
+
import { isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
7218
|
+
function envToken(name) {
|
|
7219
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
7220
|
+
}
|
|
7221
|
+
function clientTransportEnvKeys(name) {
|
|
7222
|
+
const envSegment = envToken(name);
|
|
7223
|
+
return {
|
|
7224
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
7225
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
7226
|
+
};
|
|
7227
|
+
}
|
|
7228
|
+
function credentialOverrideEnvKey(name) {
|
|
7229
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
7230
|
+
}
|
|
7231
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
7232
|
+
function credentialPointerEnvKey(name) {
|
|
7233
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
7234
|
+
}
|
|
7235
|
+
|
|
7236
|
+
class CredentialResolutionError extends Error {
|
|
7237
|
+
appName;
|
|
7238
|
+
attempted;
|
|
7239
|
+
constructor(appName, message, attempted) {
|
|
7240
|
+
super(message);
|
|
7241
|
+
this.name = "CredentialResolutionError";
|
|
7242
|
+
this.appName = appName;
|
|
7243
|
+
this.attempted = attempted;
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
class CredentialFileUnsafeError extends Error {
|
|
7248
|
+
path;
|
|
7249
|
+
constructor(path, reason) {
|
|
7250
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
7251
|
+
this.name = "CredentialFileUnsafeError";
|
|
7252
|
+
this.path = path;
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
7256
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
7257
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
7258
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
7259
|
+
var CONFIG_SUBDIR = "config";
|
|
7260
|
+
var CREDENTIALS_FILE = "credentials";
|
|
7261
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
7262
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
7263
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
7264
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
7265
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
7266
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7267
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
7268
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
7269
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
7270
|
+
function homeDir(env) {
|
|
7271
|
+
const home = env.HOME?.trim();
|
|
7272
|
+
return home ? home : null;
|
|
7273
|
+
}
|
|
7274
|
+
function absoluteOverride(env, key) {
|
|
7275
|
+
const value = env[key]?.trim();
|
|
7276
|
+
return value && isAbsolute2(value) ? value : null;
|
|
7277
|
+
}
|
|
7278
|
+
function hasnaHomeDir(env) {
|
|
7279
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
7280
|
+
if (override)
|
|
7281
|
+
return override;
|
|
7282
|
+
const home = homeDir(env);
|
|
7283
|
+
return home ? join9(home, HASNA_HOME_DIR) : null;
|
|
7284
|
+
}
|
|
7285
|
+
function appConfigDir(name, env) {
|
|
7286
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
7287
|
+
if (configRoot)
|
|
7288
|
+
return join9(configRoot, name);
|
|
7289
|
+
const root = hasnaHomeDir(env);
|
|
7290
|
+
return root ? join9(root, name, CONFIG_SUBDIR) : null;
|
|
7291
|
+
}
|
|
7292
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
7293
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
7294
|
+
return [];
|
|
7295
|
+
const directory = appConfigDir(name, env);
|
|
7296
|
+
if (!directory)
|
|
7297
|
+
return [];
|
|
7298
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
7299
|
+
return [{ path: join9(directory, file), tier: "disk" }];
|
|
7300
|
+
}
|
|
7301
|
+
function credentialDiskSources(name, env) {
|
|
7302
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
7303
|
+
}
|
|
7304
|
+
function profileDiskSources(name, env, profile) {
|
|
7305
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
7306
|
+
}
|
|
7307
|
+
function parseEnvFile(text) {
|
|
7308
|
+
const values = new Map;
|
|
7309
|
+
const unusable = new Set;
|
|
7310
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
7311
|
+
const line = rawLine.trim();
|
|
7312
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
7313
|
+
continue;
|
|
7314
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
7315
|
+
const equals = withoutExport.indexOf("=");
|
|
7316
|
+
if (equals <= 0)
|
|
7317
|
+
continue;
|
|
7318
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
7319
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
7320
|
+
continue;
|
|
7321
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
7322
|
+
const quote = value[0];
|
|
7323
|
+
if (quote === '"' || quote === "'") {
|
|
7324
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
7325
|
+
unusable.add(key);
|
|
7326
|
+
continue;
|
|
7327
|
+
}
|
|
7328
|
+
value = value.slice(1, -1);
|
|
7329
|
+
}
|
|
7330
|
+
if (value.trim().length === 0) {
|
|
7331
|
+
unusable.add(key);
|
|
7332
|
+
continue;
|
|
7333
|
+
}
|
|
7334
|
+
if (values.has(key) && values.get(key) !== value)
|
|
7335
|
+
unusable.add(key);
|
|
7336
|
+
values.set(key, value);
|
|
7337
|
+
}
|
|
7338
|
+
return { values, unusable };
|
|
7339
|
+
}
|
|
7340
|
+
function configFileModeAllowed(mode) {
|
|
7341
|
+
const permissions = mode & 4095;
|
|
7342
|
+
return permissions === 256 || permissions === 384;
|
|
7343
|
+
}
|
|
7344
|
+
function configFileReadsCoherent(before, after) {
|
|
7345
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
7346
|
+
}
|
|
7347
|
+
function readAppConfigFile(path) {
|
|
7348
|
+
const unsafe = (reason) => {
|
|
7349
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
7350
|
+
};
|
|
7351
|
+
let fd = -1;
|
|
7352
|
+
try {
|
|
7353
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
7354
|
+
} catch (error) {
|
|
7355
|
+
const code = error.code;
|
|
7356
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
7357
|
+
return null;
|
|
7358
|
+
if (code === "ELOOP")
|
|
7359
|
+
unsafe("the path is a symlink");
|
|
7360
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
7361
|
+
}
|
|
7362
|
+
try {
|
|
7363
|
+
const before = fstatSync(fd);
|
|
7364
|
+
if (!before.isFile())
|
|
7365
|
+
unsafe("the path is not a regular file");
|
|
7366
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
7367
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
7368
|
+
}
|
|
7369
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
7370
|
+
if (uid !== undefined && before.uid !== uid)
|
|
7371
|
+
unsafe("the file is not owned by the current user");
|
|
7372
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
7373
|
+
unsafe("the file exceeds the size limit");
|
|
7374
|
+
const bytes = readFileSync6(fd);
|
|
7375
|
+
const after = fstatSync(fd);
|
|
7376
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
7377
|
+
unsafe("the file changed while being read");
|
|
7378
|
+
}
|
|
7379
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
7380
|
+
} finally {
|
|
7381
|
+
if (fd !== -1)
|
|
7382
|
+
closeSync(fd);
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
7386
|
+
const parsed = readAppConfigFile(path);
|
|
7387
|
+
if (!parsed)
|
|
7388
|
+
return null;
|
|
7389
|
+
for (const key of apiKeyKeys) {
|
|
7390
|
+
if (parsed.unusable.has(key)) {
|
|
7391
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7395
|
+
if (new Set(values).size > 1) {
|
|
7396
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
7397
|
+
}
|
|
7398
|
+
return values[0] ?? null;
|
|
7399
|
+
}
|
|
7400
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
7401
|
+
function appConfigDiskValue(name, env, keys) {
|
|
7402
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
7403
|
+
if (wanted.length === 0)
|
|
7404
|
+
return null;
|
|
7405
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
7406
|
+
const parsed = readAppConfigFile(path);
|
|
7407
|
+
if (!parsed)
|
|
7408
|
+
continue;
|
|
7409
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
7410
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
7411
|
+
}
|
|
7412
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7413
|
+
if (new Set(values).size > 1)
|
|
7414
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
7415
|
+
for (const key of wanted) {
|
|
7416
|
+
if (parsed.unusable.has(key))
|
|
7417
|
+
return { key, value: "", path, unusable: true };
|
|
7418
|
+
const value = parsed.values.get(key)?.trim();
|
|
7419
|
+
if (value)
|
|
7420
|
+
return { key, value, path };
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
return null;
|
|
7424
|
+
}
|
|
7425
|
+
function assertUsableCredential(appName, source, value) {
|
|
7426
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
7427
|
+
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]);
|
|
7428
|
+
}
|
|
7429
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
7430
|
+
return;
|
|
7431
|
+
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]);
|
|
7432
|
+
}
|
|
7433
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
7434
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
7435
|
+
function sealCredential(fields) {
|
|
7436
|
+
const { apiKey } = fields;
|
|
7437
|
+
const visible = {
|
|
7438
|
+
tier: fields.tier,
|
|
7439
|
+
source: fields.source,
|
|
7440
|
+
deliberate: fields.deliberate,
|
|
7441
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
7442
|
+
warning: fields.warning
|
|
7443
|
+
};
|
|
7444
|
+
const sealed = { ...visible };
|
|
7445
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
7446
|
+
value: apiKey,
|
|
7447
|
+
enumerable: false,
|
|
7448
|
+
writable: false,
|
|
7449
|
+
configurable: false
|
|
7450
|
+
});
|
|
7451
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
7452
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
7453
|
+
value: fields.pointerVaultKey,
|
|
7454
|
+
enumerable: false,
|
|
7455
|
+
writable: false,
|
|
7456
|
+
configurable: false
|
|
7457
|
+
});
|
|
7458
|
+
}
|
|
7459
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
7460
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
7461
|
+
enumerable: false,
|
|
7462
|
+
writable: false,
|
|
7463
|
+
configurable: false
|
|
7464
|
+
});
|
|
7465
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
7466
|
+
value: true,
|
|
7467
|
+
enumerable: false,
|
|
7468
|
+
writable: false,
|
|
7469
|
+
configurable: false
|
|
7470
|
+
});
|
|
7471
|
+
return Object.freeze(sealed);
|
|
7472
|
+
}
|
|
7473
|
+
function firstEnvValue(env, keys) {
|
|
7474
|
+
for (const key of keys) {
|
|
7475
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
7476
|
+
continue;
|
|
7477
|
+
const value = env[key]?.trim();
|
|
7478
|
+
if (value)
|
|
7479
|
+
return { key, value };
|
|
7480
|
+
}
|
|
7481
|
+
return null;
|
|
7482
|
+
}
|
|
7483
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7484
|
+
function isAmbientEnvironment(env) {
|
|
7485
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
7486
|
+
}
|
|
7487
|
+
function defaultKeychainRunner(argv) {
|
|
7488
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
7489
|
+
encoding: "utf8",
|
|
7490
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7491
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
7492
|
+
});
|
|
7493
|
+
return {
|
|
7494
|
+
status: result.status,
|
|
7495
|
+
stdout: result.stdout ?? "",
|
|
7496
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
7497
|
+
};
|
|
7498
|
+
}
|
|
7499
|
+
function keychainTierEnabled(env, options) {
|
|
7500
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
7501
|
+
return false;
|
|
7502
|
+
if (options.enabled !== undefined)
|
|
7503
|
+
return options.enabled;
|
|
7504
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
7505
|
+
}
|
|
7506
|
+
function keychainAccount(env, options) {
|
|
7507
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
7508
|
+
if (station)
|
|
7509
|
+
return station;
|
|
7510
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
7511
|
+
if (host)
|
|
7512
|
+
return host;
|
|
7513
|
+
const user = env.USER?.trim();
|
|
7514
|
+
return user || null;
|
|
7515
|
+
}
|
|
7516
|
+
function keychainFailureHint(text) {
|
|
7517
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
7518
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
7519
|
+
return clean ? `: ${clean}` : "";
|
|
7520
|
+
}
|
|
7521
|
+
function readKeychainItem(name, env, kind, options) {
|
|
7522
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
7523
|
+
return null;
|
|
7524
|
+
const account = keychainAccount(env, options);
|
|
7525
|
+
if (!account)
|
|
7526
|
+
return null;
|
|
7527
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
7528
|
+
const source = `keychain:${service}@${account}`;
|
|
7529
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
7530
|
+
let result;
|
|
7531
|
+
try {
|
|
7532
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
7533
|
+
} catch (error) {
|
|
7534
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
7535
|
+
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]);
|
|
7536
|
+
}
|
|
7537
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
7538
|
+
return null;
|
|
7539
|
+
if (result.status !== 0) {
|
|
7540
|
+
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]);
|
|
7541
|
+
}
|
|
7542
|
+
const value = result.stdout.trim();
|
|
7543
|
+
if (!value) {
|
|
7544
|
+
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]);
|
|
7545
|
+
}
|
|
7546
|
+
return { value, source };
|
|
7547
|
+
}
|
|
7548
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
7549
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
7550
|
+
}
|
|
7551
|
+
function snapshotClientEnvironment(name, env) {
|
|
7552
|
+
const keys = clientTransportEnvKeys(name);
|
|
7553
|
+
const ambient = isAmbientEnvironment(env);
|
|
7554
|
+
const snapshot = Object.create(null);
|
|
7555
|
+
for (const key of [
|
|
7556
|
+
...keys.apiUrlKeys,
|
|
7557
|
+
...keys.apiKeyKeys,
|
|
7558
|
+
credentialOverrideEnvKey(name),
|
|
7559
|
+
credentialPointerEnvKey(name),
|
|
7560
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
7561
|
+
"HOME",
|
|
7562
|
+
HASNA_HOME_ENV_KEY,
|
|
7563
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
7564
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
7565
|
+
"USER"
|
|
7566
|
+
]) {
|
|
7567
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
7568
|
+
if (!descriptor)
|
|
7569
|
+
continue;
|
|
7570
|
+
if (!("value" in descriptor)) {
|
|
7571
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
7572
|
+
}
|
|
7573
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
7574
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
7575
|
+
}
|
|
7576
|
+
snapshot[key] = descriptor.value;
|
|
7577
|
+
}
|
|
7578
|
+
if (ambient) {
|
|
7579
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
7580
|
+
value: true,
|
|
7581
|
+
enumerable: false,
|
|
7582
|
+
writable: false,
|
|
7583
|
+
configurable: false
|
|
7584
|
+
});
|
|
7585
|
+
}
|
|
7586
|
+
return Object.freeze(snapshot);
|
|
7587
|
+
}
|
|
7588
|
+
function resolveCredential(name, env, options = {}) {
|
|
7589
|
+
env = snapshotClientEnvironment(name, env);
|
|
7590
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
7591
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
7592
|
+
if (options.apiKey !== undefined) {
|
|
7593
|
+
const explicitKey = options.apiKey.trim();
|
|
7594
|
+
if (!explicitKey) {
|
|
7595
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
7596
|
+
}
|
|
7597
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
7598
|
+
return sealCredential({
|
|
7599
|
+
apiKey: explicitKey,
|
|
7600
|
+
tier: "argument",
|
|
7601
|
+
source: "explicit apiKey argument",
|
|
7602
|
+
deliberate: true,
|
|
7603
|
+
diskCandidates: diskPaths,
|
|
7604
|
+
warning: null
|
|
7605
|
+
});
|
|
7606
|
+
}
|
|
7607
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
7608
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
7609
|
+
if (overrideRaw !== undefined) {
|
|
7610
|
+
const override = overrideRaw.trim();
|
|
7611
|
+
if (!override) {
|
|
7612
|
+
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]);
|
|
7613
|
+
}
|
|
7614
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
7615
|
+
return sealCredential({
|
|
7616
|
+
apiKey: override,
|
|
7617
|
+
tier: "override",
|
|
7618
|
+
source: overrideKeyName,
|
|
7619
|
+
deliberate: true,
|
|
7620
|
+
diskCandidates: diskPaths,
|
|
7621
|
+
warning: null
|
|
7622
|
+
});
|
|
7623
|
+
}
|
|
7624
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
7625
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
7626
|
+
if (pointerRaw !== undefined) {
|
|
7627
|
+
const pointer = pointerRaw.trim();
|
|
7628
|
+
if (!pointer) {
|
|
7629
|
+
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]);
|
|
7630
|
+
}
|
|
7631
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
7632
|
+
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]);
|
|
7633
|
+
}
|
|
7634
|
+
return sealCredential({
|
|
7635
|
+
apiKey: "",
|
|
7636
|
+
pointerVaultKey: pointer,
|
|
7637
|
+
tier: "pointer",
|
|
7638
|
+
source: pointerKeyName,
|
|
7639
|
+
deliberate: true,
|
|
7640
|
+
diskCandidates: diskPaths,
|
|
7641
|
+
warning: null
|
|
7642
|
+
});
|
|
7643
|
+
}
|
|
7644
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
7645
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
7646
|
+
}
|
|
7647
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
7648
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
7649
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
7650
|
+
}
|
|
7651
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
7652
|
+
if (profile) {
|
|
7653
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
7654
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
7655
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
7656
|
+
}
|
|
7657
|
+
const paths = profileDiskSources(name, env, profile);
|
|
7658
|
+
for (const path of paths) {
|
|
7659
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
7660
|
+
if (value) {
|
|
7661
|
+
assertUsableCredential(name, path, value);
|
|
7662
|
+
return sealCredential({
|
|
7663
|
+
apiKey: value,
|
|
7664
|
+
tier: "profile",
|
|
7665
|
+
source: path,
|
|
7666
|
+
deliberate: true,
|
|
7667
|
+
diskCandidates: paths,
|
|
7668
|
+
warning: null
|
|
7669
|
+
});
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
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);
|
|
7673
|
+
}
|
|
7674
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
7675
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
7676
|
+
if (blankEnv) {
|
|
7677
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
7678
|
+
}
|
|
7679
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
7680
|
+
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));
|
|
7681
|
+
}
|
|
7682
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
7683
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
7684
|
+
if (keychainHit) {
|
|
7685
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
7686
|
+
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;
|
|
7687
|
+
return sealCredential({
|
|
7688
|
+
apiKey: keychainHit.value,
|
|
7689
|
+
tier: "keychain",
|
|
7690
|
+
source: keychainHit.source,
|
|
7691
|
+
deliberate: false,
|
|
7692
|
+
diskCandidates: diskPaths,
|
|
7693
|
+
warning
|
|
7694
|
+
});
|
|
7695
|
+
}
|
|
7696
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
7697
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
7698
|
+
if (diskHits.length > 0) {
|
|
7699
|
+
const winner = diskHits[0];
|
|
7700
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
7701
|
+
const divergentSources = [
|
|
7702
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
7703
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
7704
|
+
];
|
|
7705
|
+
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;
|
|
7706
|
+
return sealCredential({
|
|
7707
|
+
apiKey: winner.value,
|
|
7708
|
+
tier: winner.src.tier,
|
|
7709
|
+
source: winner.src.path,
|
|
7710
|
+
deliberate: false,
|
|
7711
|
+
diskCandidates: diskPaths,
|
|
7712
|
+
warning
|
|
7713
|
+
});
|
|
7714
|
+
}
|
|
7715
|
+
if (envHit) {
|
|
7716
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
7717
|
+
return sealCredential({
|
|
7718
|
+
apiKey: envHit.value,
|
|
7719
|
+
tier: "env",
|
|
7720
|
+
source: envHit.key,
|
|
7721
|
+
deliberate: false,
|
|
7722
|
+
diskCandidates: diskPaths,
|
|
7723
|
+
warning: null
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
return null;
|
|
7727
|
+
}
|
|
7728
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
7729
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
7730
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
7731
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
7732
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
7733
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
7734
|
+
}
|
|
7735
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7736
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7737
|
+
function isValidDnsDomain(value) {
|
|
7738
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
7739
|
+
return false;
|
|
7740
|
+
}
|
|
7741
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
7742
|
+
}
|
|
7743
|
+
function validateAppSlug(name) {
|
|
7744
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
7745
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
7746
|
+
}
|
|
7747
|
+
return name;
|
|
7748
|
+
}
|
|
7749
|
+
function rawAuthority(value) {
|
|
7750
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
7751
|
+
if (!match)
|
|
7752
|
+
throw new Error("API URL must be absolute.");
|
|
7753
|
+
const afterScheme = value.slice(match[0].length);
|
|
7754
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
7755
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
7756
|
+
if (!authority)
|
|
7757
|
+
throw new Error("API URL must include a hostname.");
|
|
7758
|
+
return authority;
|
|
7759
|
+
}
|
|
7760
|
+
function assertCanonicalPort(port) {
|
|
7761
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
7762
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7763
|
+
}
|
|
7764
|
+
const numericPort = Number(port);
|
|
7765
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
7766
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
function canonicalAuthorityHostname(authority) {
|
|
7770
|
+
let rawHostname;
|
|
7771
|
+
if (authority.startsWith("[")) {
|
|
7772
|
+
const closingBracket = authority.indexOf("]");
|
|
7773
|
+
if (closingBracket === -1) {
|
|
7774
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
7775
|
+
}
|
|
7776
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
7777
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
7778
|
+
if (portSuffix) {
|
|
7779
|
+
if (!portSuffix.startsWith(":")) {
|
|
7780
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
7781
|
+
}
|
|
7782
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
7783
|
+
}
|
|
7784
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
7785
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
7786
|
+
}
|
|
7787
|
+
} else {
|
|
7788
|
+
const firstColon = authority.indexOf(":");
|
|
7789
|
+
const lastColon = authority.lastIndexOf(":");
|
|
7790
|
+
if (firstColon !== lastColon) {
|
|
7791
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
7792
|
+
}
|
|
7793
|
+
if (lastColon !== -1) {
|
|
7794
|
+
const port = authority.slice(lastColon + 1);
|
|
7795
|
+
assertCanonicalPort(port);
|
|
7796
|
+
rawHostname = authority.slice(0, lastColon);
|
|
7797
|
+
} else {
|
|
7798
|
+
rawHostname = authority;
|
|
7799
|
+
}
|
|
7800
|
+
const ipVersion = isIP(rawHostname);
|
|
7801
|
+
const numericAddressParts = rawHostname.split(".");
|
|
7802
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
7803
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
7804
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
return rawHostname.toLowerCase();
|
|
7808
|
+
}
|
|
7809
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
7810
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
7811
|
+
}
|
|
7812
|
+
function toV1BaseUrl(apiUrl) {
|
|
7813
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
7814
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
7815
|
+
}
|
|
7816
|
+
const input = apiUrl.trim();
|
|
7817
|
+
const authority = rawAuthority(input);
|
|
7818
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
7819
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
7820
|
+
}
|
|
7821
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
7822
|
+
const url = new URL(input);
|
|
7823
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7824
|
+
throw new Error("API URL must use http or https.");
|
|
7825
|
+
}
|
|
7826
|
+
if (url.username || url.password) {
|
|
7827
|
+
throw new Error("API URL must not include credentials.");
|
|
7828
|
+
}
|
|
7829
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
7830
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
7831
|
+
}
|
|
7832
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
7833
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
7834
|
+
}
|
|
7835
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
7836
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
7837
|
+
}
|
|
7838
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
7839
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
7840
|
+
}
|
|
7841
|
+
if (url.search || url.hash) {
|
|
7842
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
7843
|
+
}
|
|
7844
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
7845
|
+
if (path.endsWith("/v1"))
|
|
7846
|
+
path = path.slice(0, -"/v1".length);
|
|
7847
|
+
url.pathname = `${path}/v1`;
|
|
7848
|
+
return url.toString().replace(/\/+$/, "");
|
|
7849
|
+
}
|
|
7850
|
+
class ClientTransportConfigurationError extends Error {
|
|
7851
|
+
appName;
|
|
7852
|
+
sources;
|
|
7853
|
+
constructor(appName, message, sources = []) {
|
|
7854
|
+
super(message);
|
|
7855
|
+
this.name = "ClientTransportConfigurationError";
|
|
7856
|
+
this.appName = appName;
|
|
7857
|
+
this.sources = Object.freeze([...sources]);
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
7861
|
+
env = snapshotClientEnvironment(name, env);
|
|
7862
|
+
const keys = clientTransportEnvKeys(name);
|
|
7863
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
7864
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
7865
|
+
if (blankUrl) {
|
|
7866
|
+
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]);
|
|
7867
|
+
}
|
|
7868
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
7869
|
+
if (controlledUrl) {
|
|
7870
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
7871
|
+
}
|
|
7872
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
7873
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
7874
|
+
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));
|
|
7875
|
+
}
|
|
7876
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
7877
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
7878
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
7879
|
+
if (diskConfigUrlHit?.unusable) {
|
|
7880
|
+
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]);
|
|
7881
|
+
}
|
|
7882
|
+
const urlCandidates = [
|
|
7883
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
7884
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
7885
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
7886
|
+
];
|
|
7887
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
7888
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
7889
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
7890
|
+
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));
|
|
7891
|
+
}
|
|
7892
|
+
const warnings = [];
|
|
7893
|
+
if (configuredUrl && !envUrlHit) {
|
|
7894
|
+
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.`);
|
|
7895
|
+
}
|
|
7896
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
7897
|
+
if (!credential) {
|
|
7898
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
7899
|
+
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`;
|
|
7900
|
+
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.`);
|
|
7901
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
7902
|
+
}
|
|
7903
|
+
if (credential.warning)
|
|
7904
|
+
warnings.push(credential.warning);
|
|
7905
|
+
let urlHit;
|
|
7906
|
+
if (configuredUrl) {
|
|
7907
|
+
urlHit = configuredUrl;
|
|
7908
|
+
} else {
|
|
7909
|
+
try {
|
|
7910
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
7911
|
+
} catch (error) {
|
|
7912
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7913
|
+
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]]);
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
const apiUrlSource = urlHit.key;
|
|
7917
|
+
let baseUrl;
|
|
7918
|
+
try {
|
|
7919
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
7920
|
+
} catch (error) {
|
|
7921
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7922
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
7923
|
+
}
|
|
7924
|
+
return {
|
|
7925
|
+
resolution: {
|
|
7926
|
+
transport: "http",
|
|
7927
|
+
transportSource: urlHit.key,
|
|
7928
|
+
baseUrl,
|
|
7929
|
+
apiUrlSource,
|
|
7930
|
+
apiKeyPresent: true,
|
|
7931
|
+
apiKeySource: credential.source,
|
|
7932
|
+
apiKeyTier: credential.tier,
|
|
7933
|
+
misconfigured: false,
|
|
7934
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
7935
|
+
},
|
|
7936
|
+
credential
|
|
7937
|
+
};
|
|
7938
|
+
}
|
|
7939
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
7940
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
7941
|
+
}
|
|
7942
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
7943
|
+
const paths = credentialDiskSources(name, env);
|
|
7944
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7945
|
+
}
|
|
7946
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7947
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
7948
|
+
"host",
|
|
7949
|
+
":authority",
|
|
7950
|
+
"forwarded",
|
|
7951
|
+
"x-forwarded-host",
|
|
7952
|
+
"x-original-host"
|
|
7953
|
+
]);
|
|
7954
|
+
|
|
7955
|
+
// src/lib/transport.ts
|
|
7956
|
+
init_config();
|
|
7957
|
+
|
|
7958
|
+
// src/lib/local-opt-in.ts
|
|
7959
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
7960
|
+
function isHooksLocalOptIn(env = process.env) {
|
|
7961
|
+
return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
7962
|
+
}
|
|
7963
|
+
function hooksAuthorityEnvKeys() {
|
|
7964
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
7965
|
+
return [
|
|
7966
|
+
...keys.apiUrlKeys,
|
|
7967
|
+
...keys.apiKeyKeys,
|
|
7968
|
+
credentialOverrideEnvKey("hooks"),
|
|
7969
|
+
credentialPointerEnvKey("hooks"),
|
|
7970
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
7971
|
+
];
|
|
7972
|
+
}
|
|
7973
|
+
function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
7974
|
+
return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
7975
|
+
}
|
|
7976
|
+
function selectsHooksLocalStore(env = process.env) {
|
|
7977
|
+
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
7978
|
+
}
|
|
7979
|
+
function hooksResolverEnv(env) {
|
|
7980
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
7981
|
+
if (blanks.length === 0)
|
|
7982
|
+
return env;
|
|
7983
|
+
const next = { ...env };
|
|
7984
|
+
for (const key of blanks)
|
|
7985
|
+
delete next[key];
|
|
7986
|
+
return next;
|
|
7987
|
+
}
|
|
7988
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7989
|
+
function isAmbientHooksEnv(env) {
|
|
7990
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
7991
|
+
return true;
|
|
7992
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
7993
|
+
}
|
|
7994
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
7995
|
+
const normalised = hooksResolverEnv(env);
|
|
7996
|
+
if (normalised === env)
|
|
7997
|
+
return { env: normalised, credentials };
|
|
7998
|
+
const keychain = { ...credentials.keychain };
|
|
7999
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
8000
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
8001
|
+
}
|
|
8002
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
8003
|
+
}
|
|
8004
|
+
|
|
8005
|
+
// src/lib/transport.ts
|
|
8006
|
+
var localNoticePrinted = false;
|
|
8007
|
+
function announceLocal(notice, reason) {
|
|
8008
|
+
if (localNoticePrinted)
|
|
8009
|
+
return;
|
|
8010
|
+
localNoticePrinted = true;
|
|
8011
|
+
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.`;
|
|
8012
|
+
if (notice)
|
|
8013
|
+
notice(line);
|
|
8014
|
+
else if (typeof process !== "undefined")
|
|
8015
|
+
process.stderr.write(`${line}
|
|
8016
|
+
`);
|
|
8017
|
+
}
|
|
8018
|
+
function hooksRegistryOrigin(v1BaseUrl) {
|
|
8019
|
+
return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
8020
|
+
}
|
|
8021
|
+
function rethrowHooksAuthorityFailure(error) {
|
|
8022
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8023
|
+
const name = error instanceof Error ? error.name : "";
|
|
8024
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
8025
|
+
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 });
|
|
8026
|
+
}
|
|
8027
|
+
if (/no API key could be resolved/.test(message)) {
|
|
8028
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
8029
|
+
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 });
|
|
8030
|
+
}
|
|
8031
|
+
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 });
|
|
8032
|
+
}
|
|
8033
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
8034
|
+
}
|
|
8035
|
+
function resolveHooksTransport(env = process.env, options = {}) {
|
|
8036
|
+
if (selectsHooksLocalStore(env)) {
|
|
8037
|
+
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
8038
|
+
return { mode: "local", source: "local-opt-in", authority: null };
|
|
8039
|
+
}
|
|
8040
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8041
|
+
const tier1 = options.credentials?.apiKey;
|
|
8042
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
8043
|
+
let resolution;
|
|
8044
|
+
let credential;
|
|
8045
|
+
try {
|
|
8046
|
+
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
8047
|
+
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
8048
|
+
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
8049
|
+
});
|
|
8050
|
+
} catch (error) {
|
|
8051
|
+
rethrowHooksAuthorityFailure(error);
|
|
8052
|
+
}
|
|
8053
|
+
if (!credential) {
|
|
8054
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
8055
|
+
}
|
|
8056
|
+
return {
|
|
8057
|
+
mode: "remote",
|
|
8058
|
+
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
8059
|
+
authority: {
|
|
8060
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
8061
|
+
apiKey: credential.apiKey,
|
|
8062
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
8063
|
+
apiKeySource: credential.source,
|
|
8064
|
+
apiKeyTier: credential.tier,
|
|
8065
|
+
v1BaseUrl: resolution.baseUrl,
|
|
8066
|
+
warning: resolution.warning
|
|
8067
|
+
}
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
8071
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8072
|
+
const tier1 = options.credentials?.apiKey;
|
|
8073
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
8074
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
8075
|
+
}
|
|
8076
|
+
|
|
8077
|
+
// src/index.ts
|
|
7543
8078
|
init_store();
|
|
7544
8079
|
init_manifest();
|
|
7545
|
-
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
7546
8080
|
|
|
7547
8081
|
// src/lib/custom-install.ts
|
|
7548
8082
|
init_config();
|
|
7549
8083
|
init_manifest();
|
|
7550
|
-
import { existsSync as
|
|
7551
|
-
import { join as
|
|
8084
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
|
|
8085
|
+
import { join as join10 } from "path";
|
|
7552
8086
|
import { tmpdir } from "os";
|
|
7553
8087
|
function manifestAt(dir) {
|
|
7554
|
-
const path =
|
|
7555
|
-
if (!
|
|
8088
|
+
const path = join10(dir, "manifest.json");
|
|
8089
|
+
if (!existsSync9(path))
|
|
7556
8090
|
return;
|
|
7557
8091
|
try {
|
|
7558
|
-
return parseManifest(
|
|
8092
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
7559
8093
|
} catch {
|
|
7560
8094
|
return;
|
|
7561
8095
|
}
|
|
@@ -7574,8 +8108,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7574
8108
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7575
8109
|
}
|
|
7576
8110
|
async function cloneToTemp(gitUrl) {
|
|
7577
|
-
const tmp = mkdtempSync(
|
|
7578
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8111
|
+
const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
|
|
8112
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
|
|
7579
8113
|
stdout: "pipe",
|
|
7580
8114
|
stderr: "pipe"
|
|
7581
8115
|
});
|
|
@@ -7585,10 +8119,10 @@ async function cloneToTemp(gitUrl) {
|
|
|
7585
8119
|
proc.exited
|
|
7586
8120
|
]);
|
|
7587
8121
|
if (exitCode !== 0) {
|
|
7588
|
-
|
|
8122
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
7589
8123
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7590
8124
|
}
|
|
7591
|
-
return
|
|
8125
|
+
return join10(tmp, "repo");
|
|
7592
8126
|
}
|
|
7593
8127
|
function isGitUrl(value) {
|
|
7594
8128
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7598,7 +8132,7 @@ function isCustomSource(value) {
|
|
|
7598
8132
|
return true;
|
|
7599
8133
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7600
8134
|
return true;
|
|
7601
|
-
return
|
|
8135
|
+
return existsSync9(value);
|
|
7602
8136
|
}
|
|
7603
8137
|
async function installCustomSource(source) {
|
|
7604
8138
|
if (isGitUrl(source)) {
|
|
@@ -7606,17 +8140,17 @@ async function installCustomSource(source) {
|
|
|
7606
8140
|
try {
|
|
7607
8141
|
return copyManifestDir(repoDir, source, "git");
|
|
7608
8142
|
} finally {
|
|
7609
|
-
|
|
8143
|
+
rmSync2(repoDir, { recursive: true, force: true });
|
|
7610
8144
|
}
|
|
7611
8145
|
}
|
|
7612
8146
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7613
8147
|
return installManifestUrl(source);
|
|
7614
8148
|
}
|
|
7615
|
-
if (!
|
|
8149
|
+
if (!existsSync9(source)) {
|
|
7616
8150
|
throw new Error(`No such local path: ${source}`);
|
|
7617
8151
|
}
|
|
7618
8152
|
if (source.endsWith("manifest.json")) {
|
|
7619
|
-
const manifest = parseManifest(
|
|
8153
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
7620
8154
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7621
8155
|
}
|
|
7622
8156
|
return copyManifestDir(source, source, "local");
|
|
@@ -7647,8 +8181,7 @@ async function installManifestUrl(url) {
|
|
|
7647
8181
|
}
|
|
7648
8182
|
// src/lib/sync.ts
|
|
7649
8183
|
import { createHash as createHash2 } from "crypto";
|
|
7650
|
-
import { readFileSync as
|
|
7651
|
-
init_config();
|
|
8184
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
7652
8185
|
init_db();
|
|
7653
8186
|
init_store();
|
|
7654
8187
|
init_manifest();
|
|
@@ -7656,7 +8189,7 @@ function sha256OfText(text) {
|
|
|
7656
8189
|
return createHash2("sha256").update(text).digest("hex");
|
|
7657
8190
|
}
|
|
7658
8191
|
function sha256OfPath(path) {
|
|
7659
|
-
return createHash2("sha256").update(
|
|
8192
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
7660
8193
|
}
|
|
7661
8194
|
function collectBundledCatalog() {
|
|
7662
8195
|
const out = [];
|
|
@@ -7672,9 +8205,8 @@ function collectBundledCatalog() {
|
|
|
7672
8205
|
}
|
|
7673
8206
|
return out;
|
|
7674
8207
|
}
|
|
7675
|
-
async function fetchJson(base, path, opts) {
|
|
8208
|
+
async function fetchJson(base, path, apiKey, opts) {
|
|
7676
8209
|
const headers = { accept: "application/json" };
|
|
7677
|
-
const apiKey = resolveApiKey();
|
|
7678
8210
|
if (apiKey)
|
|
7679
8211
|
headers["x-api-key"] = apiKey;
|
|
7680
8212
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -7683,7 +8215,7 @@ async function fetchJson(base, path, opts) {
|
|
|
7683
8215
|
signal: AbortSignal.timeout(15000)
|
|
7684
8216
|
});
|
|
7685
8217
|
if (res.status === 401) {
|
|
7686
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or
|
|
8218
|
+
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
8219
|
}
|
|
7688
8220
|
if (!res.ok) {
|
|
7689
8221
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
@@ -7694,9 +8226,9 @@ async function fetchJson(base, path, opts) {
|
|
|
7694
8226
|
}
|
|
7695
8227
|
return body;
|
|
7696
8228
|
}
|
|
7697
|
-
async function fetchRemoteState(apiUrl) {
|
|
7698
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
7699
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8229
|
+
async function fetchRemoteState(apiUrl, apiKey) {
|
|
8230
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
|
|
8231
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7700
8232
|
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
7701
8233
|
throw new Error("remote catalog is malformed: missing hooks array");
|
|
7702
8234
|
}
|
|
@@ -7739,20 +8271,25 @@ function computeDiff(catalog, remoteLock) {
|
|
|
7739
8271
|
return diff;
|
|
7740
8272
|
}
|
|
7741
8273
|
async function planSync(options = {}) {
|
|
7742
|
-
const
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
8274
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8275
|
+
credentials: options.credentials
|
|
8276
|
+
});
|
|
8277
|
+
if (transport.mode === "remote" && transport.authority) {
|
|
8278
|
+
const { origin, apiKey } = transport.authority;
|
|
8279
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
8280
|
+
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
7746
8281
|
}
|
|
7747
8282
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
7748
8283
|
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
7749
8284
|
}
|
|
7750
8285
|
async function syncHooks(options = {}) {
|
|
7751
|
-
const
|
|
7752
|
-
|
|
8286
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8287
|
+
credentials: options.credentials
|
|
8288
|
+
});
|
|
8289
|
+
const plan = await planSync(options);
|
|
7753
8290
|
if (options.dryRun)
|
|
7754
8291
|
return { ...plan, dryRun: true };
|
|
7755
|
-
if (!
|
|
8292
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
7756
8293
|
const db = getDb();
|
|
7757
8294
|
for (const entry of collectBundledCatalog()) {
|
|
7758
8295
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
@@ -7767,19 +8304,20 @@ async function syncHooks(options = {}) {
|
|
|
7767
8304
|
}
|
|
7768
8305
|
return plan;
|
|
7769
8306
|
}
|
|
7770
|
-
const {
|
|
8307
|
+
const { origin, apiKey } = transport.authority;
|
|
8308
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
7771
8309
|
const diff = computeDiff(catalog, remoteLock);
|
|
7772
|
-
const staged = await stageSyncArtifacts(
|
|
7773
|
-
await commitSyncArtifacts(staged,
|
|
7774
|
-
return { apiUrl, dryRun: false, diff };
|
|
8310
|
+
const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
|
|
8311
|
+
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
8312
|
+
return { apiUrl: origin, dryRun: false, diff };
|
|
7775
8313
|
}
|
|
7776
|
-
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
8314
|
+
async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
7777
8315
|
const staged = [];
|
|
7778
8316
|
for (const name of names) {
|
|
7779
8317
|
const entry = remoteLock.hooks[name];
|
|
7780
8318
|
if (!entry)
|
|
7781
8319
|
continue;
|
|
7782
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}
|
|
8320
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
|
|
7783
8321
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7784
8322
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
7785
8323
|
}
|
|
@@ -7846,8 +8384,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7846
8384
|
throw err;
|
|
7847
8385
|
}
|
|
7848
8386
|
}
|
|
7849
|
-
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7850
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8387
|
+
async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
8388
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7851
8389
|
const entry = remoteLock.hooks?.[name];
|
|
7852
8390
|
if (!entry) {
|
|
7853
8391
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
@@ -7856,7 +8394,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7856
8394
|
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7857
8395
|
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
7858
8396
|
}
|
|
7859
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
8397
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
|
|
7860
8398
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7861
8399
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7862
8400
|
}
|
|
@@ -7910,9 +8448,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7910
8448
|
// src/serve.ts
|
|
7911
8449
|
init_manifest();
|
|
7912
8450
|
init_store();
|
|
7913
|
-
|
|
7914
|
-
import {
|
|
7915
|
-
import { dirname as dirname4, join as join12 } from "path";
|
|
8451
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8452
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
7916
8453
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7917
8454
|
|
|
7918
8455
|
// src/lib/secure-compare.ts
|
|
@@ -8073,7 +8610,7 @@ var openApiDocument = {
|
|
|
8073
8610
|
apiKey: {
|
|
8074
8611
|
type: "http",
|
|
8075
8612
|
scheme: "bearer",
|
|
8076
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
8613
|
+
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
8614
|
}
|
|
8078
8615
|
}
|
|
8079
8616
|
}
|
|
@@ -8086,8 +8623,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
8086
8623
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
8087
8624
|
function packageVersion() {
|
|
8088
8625
|
try {
|
|
8089
|
-
const pkgPath =
|
|
8090
|
-
return JSON.parse(
|
|
8626
|
+
const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8627
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
|
|
8091
8628
|
} catch {
|
|
8092
8629
|
return "0.0.0";
|
|
8093
8630
|
}
|
|
@@ -8244,12 +8781,11 @@ function resolveServeOptions(options) {
|
|
|
8244
8781
|
}
|
|
8245
8782
|
function startServeServer(options) {
|
|
8246
8783
|
const { port, host } = resolveServeOptions(options);
|
|
8247
|
-
const apiKey = resolveApiKey();
|
|
8248
8784
|
const server = Bun.serve({
|
|
8249
8785
|
hostname: host,
|
|
8250
8786
|
port,
|
|
8251
8787
|
async fetch(req) {
|
|
8252
|
-
return handleServeRequest(req,
|
|
8788
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8253
8789
|
}
|
|
8254
8790
|
});
|
|
8255
8791
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
@@ -8354,11 +8890,11 @@ async function runHook(name, input, options = {}) {
|
|
|
8354
8890
|
throw new Error(`Hook '${name}' not found`);
|
|
8355
8891
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
8356
8892
|
const resolvedScript = custom2?.scriptPath;
|
|
8357
|
-
const script = resolvedScript ?? (
|
|
8893
|
+
const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
|
|
8358
8894
|
if (!script)
|
|
8359
8895
|
throw new Error(`Hook script not found: ${name}`);
|
|
8360
8896
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
8361
|
-
const content =
|
|
8897
|
+
const content = readFileSync10(script);
|
|
8362
8898
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
8363
8899
|
if (!check.ok) {
|
|
8364
8900
|
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 +8985,6 @@ async function runHook(name, input, options = {}) {
|
|
|
8449
8985
|
export {
|
|
8450
8986
|
writeLock,
|
|
8451
8987
|
writeCustomHook,
|
|
8452
|
-
writeConfig,
|
|
8453
8988
|
verifyScriptHash,
|
|
8454
8989
|
upsertHookRecord,
|
|
8455
8990
|
updateProfile,
|
|
@@ -8470,11 +9005,11 @@ export {
|
|
|
8470
9005
|
resolveTables,
|
|
8471
9006
|
resolveScriptPath,
|
|
8472
9007
|
resolveScript,
|
|
9008
|
+
resolveHooksTransport,
|
|
9009
|
+
resolveHooksServePublishKey,
|
|
8473
9010
|
resolveHookMeta,
|
|
8474
9011
|
resolveHookDir,
|
|
8475
9012
|
resolveHook,
|
|
8476
|
-
resolveApiUrl,
|
|
8477
|
-
resolveApiKey,
|
|
8478
9013
|
removeProjectHook,
|
|
8479
9014
|
removePinnedHook,
|
|
8480
9015
|
removeHookRecord,
|
|
@@ -8482,7 +9017,6 @@ export {
|
|
|
8482
9017
|
removeHook,
|
|
8483
9018
|
readLock,
|
|
8484
9019
|
readCustomManifest,
|
|
8485
|
-
readConfig,
|
|
8486
9020
|
provisionCloudflareResources,
|
|
8487
9021
|
planSync,
|
|
8488
9022
|
pinInstalledHook,
|
|
@@ -8500,6 +9034,7 @@ export {
|
|
|
8500
9034
|
installHook,
|
|
8501
9035
|
installCustomSource,
|
|
8502
9036
|
importProfiles,
|
|
9037
|
+
hooksRegistryOrigin,
|
|
8503
9038
|
hookExists,
|
|
8504
9039
|
handleServeRequest,
|
|
8505
9040
|
getSyncMetaAll,
|
|
@@ -8524,7 +9059,6 @@ export {
|
|
|
8524
9059
|
getHookEvents,
|
|
8525
9060
|
getHook,
|
|
8526
9061
|
getCustomHooksDir,
|
|
8527
|
-
getConfigPath,
|
|
8528
9062
|
fetchPinnedHook,
|
|
8529
9063
|
exportProfiles,
|
|
8530
9064
|
deleteProfile,
|