@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/bin/serve.js
CHANGED
|
@@ -17,90 +17,83 @@ var __export = (target, all) => {
|
|
|
17
17
|
|
|
18
18
|
// src/lib/app-home.ts
|
|
19
19
|
import { existsSync } from "fs";
|
|
20
|
-
import { homedir as homedir2 } from "os";
|
|
21
|
-
import { join as join2, resolve } from "path";
|
|
22
|
-
|
|
23
|
-
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
24
20
|
import { homedir } from "os";
|
|
25
|
-
import { join } from "path";
|
|
26
|
-
|
|
21
|
+
import { join, resolve } from "path";
|
|
22
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
23
|
+
import { join as pathsResolverJoin } from "path";
|
|
24
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
27
25
|
config: "HASNA_CONFIG_HOME",
|
|
28
26
|
data: "HASNA_DATA_HOME",
|
|
29
27
|
state: "HASNA_STATE_HOME",
|
|
30
28
|
cache: "HASNA_CACHE_HOME"
|
|
31
29
|
};
|
|
32
|
-
var
|
|
33
|
-
function
|
|
30
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
31
|
+
function pathsResolverAssertApp(app) {
|
|
34
32
|
if (typeof app !== "string" || app.length === 0) {
|
|
35
33
|
throw new TypeError("paths: app must be a non-empty string");
|
|
36
34
|
}
|
|
37
|
-
if (!
|
|
35
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
38
36
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
46
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
47
|
-
}
|
|
48
|
-
function isMacOS(platform) {
|
|
49
|
-
return platform === "darwin";
|
|
39
|
+
function pathsResolverAssertKind(kind) {
|
|
40
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
41
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
42
|
+
}
|
|
50
43
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
function pathsResolverBaseDir(kind, options) {
|
|
45
|
+
pathsResolverAssertKind(kind);
|
|
46
|
+
const env = options.env ?? process.env;
|
|
47
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
48
|
+
if (typeof override === "string" && override.length > 0)
|
|
54
49
|
return override;
|
|
55
|
-
const home = options.home ??
|
|
50
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
56
51
|
const platform = options.platform ?? process.platform;
|
|
57
|
-
if (
|
|
52
|
+
if (platform === "darwin") {
|
|
58
53
|
switch (kind) {
|
|
59
54
|
case "config":
|
|
60
55
|
case "data":
|
|
61
|
-
return
|
|
56
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
62
57
|
case "cache":
|
|
63
|
-
return
|
|
58
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
64
59
|
case "state":
|
|
65
|
-
return
|
|
60
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
switch (kind) {
|
|
69
64
|
case "config":
|
|
70
|
-
return
|
|
65
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
71
66
|
case "data":
|
|
72
|
-
return
|
|
67
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
73
68
|
case "state":
|
|
74
|
-
return
|
|
69
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
75
70
|
case "cache":
|
|
76
|
-
return
|
|
71
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
77
72
|
}
|
|
78
73
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
const appSegment = options.internal === true ?
|
|
82
|
-
return
|
|
74
|
+
function pathsResolverResolve(kind, options) {
|
|
75
|
+
pathsResolverAssertApp(options.app);
|
|
76
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
77
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
83
78
|
}
|
|
84
79
|
function dataDir(options) {
|
|
85
|
-
return
|
|
80
|
+
return pathsResolverResolve("data", options);
|
|
86
81
|
}
|
|
87
|
-
|
|
88
|
-
// src/lib/app-home.ts
|
|
89
82
|
var STORE_MARKER = "hooks.db";
|
|
90
83
|
function getHomeDir(env = process.env) {
|
|
91
|
-
return env.HOME || env.USERPROFILE ||
|
|
84
|
+
return env.HOME || env.USERPROFILE || homedir();
|
|
92
85
|
}
|
|
93
86
|
function getResolverDataRoot(env = process.env) {
|
|
94
87
|
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
95
88
|
}
|
|
96
89
|
function getLegacyDataRoot(env = process.env) {
|
|
97
|
-
return
|
|
90
|
+
return join(getHomeDir(env), ".hasna", "hooks");
|
|
98
91
|
}
|
|
99
92
|
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
100
93
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
101
94
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
102
95
|
return true;
|
|
103
|
-
return existsSync(
|
|
96
|
+
return existsSync(join(resolved, STORE_MARKER));
|
|
104
97
|
}
|
|
105
98
|
function getExplicitDataDir(env = process.env) {
|
|
106
99
|
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
@@ -128,7 +121,7 @@ function getReportedDbPath(env = process.env) {
|
|
|
128
121
|
const explicit = getExplicitDbPath(env);
|
|
129
122
|
if (explicit)
|
|
130
123
|
return explicit;
|
|
131
|
-
return
|
|
124
|
+
return join(getEffectiveDataRoot(env), "hooks.db");
|
|
132
125
|
}
|
|
133
126
|
function getExplicitDbPath(env = process.env) {
|
|
134
127
|
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
@@ -189,6 +182,18 @@ var HOOKS = [
|
|
|
189
182
|
matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
190
183
|
tags: ["workspace", "repos", "structure", "guard", "safety", "orgs", "multi-agent"]
|
|
191
184
|
},
|
|
185
|
+
{
|
|
186
|
+
name: "trash-guard",
|
|
187
|
+
displayName: "Trash Guard",
|
|
188
|
+
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)",
|
|
189
|
+
version: "0.1.0",
|
|
190
|
+
category: "Git Safety",
|
|
191
|
+
event: "PreToolUse",
|
|
192
|
+
matcher: "Bash",
|
|
193
|
+
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
194
|
+
rewritesInput: true,
|
|
195
|
+
timeoutSeconds: 5
|
|
196
|
+
},
|
|
192
197
|
{
|
|
193
198
|
name: "checktests",
|
|
194
199
|
displayName: "Check Tests",
|
|
@@ -656,8 +661,8 @@ function getHook(name) {
|
|
|
656
661
|
}
|
|
657
662
|
|
|
658
663
|
// src/lib/manifest.ts
|
|
659
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync
|
|
660
|
-
import { basename, dirname, isAbsolute, join as
|
|
664
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
665
|
+
import { basename, dirname, isAbsolute, join as join3, normalize, resolve as resolve2, sep } from "path";
|
|
661
666
|
|
|
662
667
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
663
668
|
var exports_external = {};
|
|
@@ -4632,519 +4637,161 @@ var coerce = {
|
|
|
4632
4637
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
4633
4638
|
};
|
|
4634
4639
|
var NEVER = INVALID;
|
|
4635
|
-
//
|
|
4636
|
-
import {
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
function envToken(name) {
|
|
4640
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
4641
|
-
}
|
|
4642
|
-
function clientTransportEnvKeys(name) {
|
|
4643
|
-
const envSegment = envToken(name);
|
|
4644
|
-
return {
|
|
4645
|
-
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4646
|
-
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4647
|
-
};
|
|
4640
|
+
// src/config.ts
|
|
4641
|
+
import { join as join2 } from "path";
|
|
4642
|
+
function getHooksDataDir() {
|
|
4643
|
+
return getEffectiveDataRoot();
|
|
4648
4644
|
}
|
|
4649
|
-
function
|
|
4650
|
-
return
|
|
4645
|
+
function getCustomHooksDir() {
|
|
4646
|
+
return join2(getHooksDataDir(), "hooks");
|
|
4651
4647
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4648
|
+
function getLockPath() {
|
|
4649
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4650
|
+
if (explicit)
|
|
4651
|
+
return explicit;
|
|
4652
|
+
return join2(getHooksDataDir(), "hooks.lock");
|
|
4655
4653
|
}
|
|
4656
4654
|
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4655
|
+
// src/lib/semver.ts
|
|
4656
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})";
|
|
4657
|
+
var ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*";
|
|
4658
|
+
var SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4659
|
+
|
|
4660
|
+
// src/lib/manifest.ts
|
|
4661
|
+
var HOOK_NAME_RE = /^[\w-]+$/;
|
|
4662
|
+
var manifestSchema = exports_external.object({
|
|
4663
|
+
name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4664
|
+
version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4665
|
+
description: exports_external.string().optional(),
|
|
4666
|
+
events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
|
|
4667
|
+
script: exports_external.string().min(1, "script must name a relative path or inline content"),
|
|
4668
|
+
script_kind: exports_external.enum(["inline", "file"]).optional(),
|
|
4669
|
+
args: exports_external.array(exports_external.string()).optional(),
|
|
4670
|
+
timeout_ms: exports_external.number().int().positive().optional(),
|
|
4671
|
+
env: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
4672
|
+
});
|
|
4673
|
+
function parseManifest(json) {
|
|
4674
|
+
const parsed = JSON.parse(json);
|
|
4675
|
+
return manifestSchema.parse(parsed);
|
|
4666
4676
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
var LEGACY_CLOUD_DIR = "cloud";
|
|
4670
|
-
var CONFIG_DIR = ".config";
|
|
4671
|
-
var CONFIG_NAMESPACE = "hasna";
|
|
4672
|
-
var LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01";
|
|
4673
|
-
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4674
|
-
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4675
|
-
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4676
|
-
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4677
|
-
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
4678
|
-
function homeDir(env) {
|
|
4679
|
-
const home = env.HOME?.trim();
|
|
4680
|
-
return home ? home : null;
|
|
4677
|
+
function shortManifestName(name) {
|
|
4678
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4681
4679
|
}
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
return [
|
|
4689
|
-
{
|
|
4690
|
-
path: join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4691
|
-
tier: "fleet-env",
|
|
4692
|
-
deprecated: false
|
|
4693
|
-
},
|
|
4694
|
-
{
|
|
4695
|
-
path: join3(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
|
|
4696
|
-
tier: "legacy-cloud",
|
|
4697
|
-
deprecated: true
|
|
4698
|
-
},
|
|
4699
|
-
{
|
|
4700
|
-
path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
|
|
4701
|
-
tier: "config",
|
|
4702
|
-
deprecated: false
|
|
4703
|
-
},
|
|
4704
|
-
{
|
|
4705
|
-
path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
|
|
4706
|
-
tier: "config-legacy",
|
|
4707
|
-
deprecated: true
|
|
4708
|
-
}
|
|
4709
|
-
];
|
|
4680
|
+
|
|
4681
|
+
class ScriptContainmentError extends Error {
|
|
4682
|
+
constructor(path, hookDir, raw) {
|
|
4683
|
+
super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
|
|
4684
|
+
this.name = "ScriptContainmentError";
|
|
4685
|
+
}
|
|
4710
4686
|
}
|
|
4711
|
-
function
|
|
4712
|
-
return
|
|
4687
|
+
function isWithin(child, parent) {
|
|
4688
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4713
4689
|
}
|
|
4714
|
-
function
|
|
4715
|
-
|
|
4690
|
+
function assertContainedLexical(target, root, raw) {
|
|
4691
|
+
const lexical = resolve2(target);
|
|
4692
|
+
const rootLexical = resolve2(root);
|
|
4693
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4694
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4695
|
+
}
|
|
4696
|
+
return lexical;
|
|
4716
4697
|
}
|
|
4717
|
-
function
|
|
4718
|
-
const
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
continue;
|
|
4727
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
4728
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4729
|
-
continue;
|
|
4730
|
-
let value = withoutExport.slice(equals + 1).trim();
|
|
4731
|
-
const quote = value[0];
|
|
4732
|
-
if (quote === '"' || quote === "'") {
|
|
4733
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
4734
|
-
continue;
|
|
4735
|
-
value = value.slice(1, -1);
|
|
4736
|
-
}
|
|
4737
|
-
if (value.length === 0)
|
|
4738
|
-
continue;
|
|
4739
|
-
values.set(key, value);
|
|
4698
|
+
function realpathNearest(path) {
|
|
4699
|
+
const tail = [];
|
|
4700
|
+
let cur = path;
|
|
4701
|
+
while (!existsSync2(cur)) {
|
|
4702
|
+
const parent = dirname(cur);
|
|
4703
|
+
if (parent === cur)
|
|
4704
|
+
break;
|
|
4705
|
+
tail.unshift(basename(cur));
|
|
4706
|
+
cur = parent;
|
|
4740
4707
|
}
|
|
4741
|
-
return
|
|
4708
|
+
return join3(realpathSync(cur), ...tail);
|
|
4742
4709
|
}
|
|
4743
|
-
function
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
return null;
|
|
4749
|
-
text = readFileSync(path, "utf8");
|
|
4750
|
-
} catch {
|
|
4751
|
-
return null;
|
|
4710
|
+
function assertContainedReal(root, target, raw) {
|
|
4711
|
+
const rootReal = realpathSync(root);
|
|
4712
|
+
const targetReal = realpathNearest(target);
|
|
4713
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4714
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4752
4715
|
}
|
|
4753
|
-
return parseEnvFile(text);
|
|
4754
4716
|
}
|
|
4755
|
-
function
|
|
4756
|
-
const
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4717
|
+
function assertContained(target, root, raw) {
|
|
4718
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4719
|
+
assertContainedReal(root, lexical, raw);
|
|
4720
|
+
return lexical;
|
|
4721
|
+
}
|
|
4722
|
+
function isInlineScript(manifest) {
|
|
4723
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4724
|
+
`);
|
|
4725
|
+
}
|
|
4726
|
+
function scriptRelFor(manifest) {
|
|
4727
|
+
if (isInlineScript(manifest)) {
|
|
4728
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4729
|
+
return `script${ext}`;
|
|
4763
4730
|
}
|
|
4764
|
-
|
|
4731
|
+
const rel = normalize(manifest.script);
|
|
4732
|
+
if (isAbsolute(rel)) {
|
|
4733
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4734
|
+
}
|
|
4735
|
+
return rel;
|
|
4765
4736
|
}
|
|
4766
|
-
function
|
|
4767
|
-
if (
|
|
4768
|
-
|
|
4737
|
+
function resolveScript(manifest, manifestDir) {
|
|
4738
|
+
if (isInlineScript(manifest))
|
|
4739
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4740
|
+
const rel = scriptRelFor(manifest);
|
|
4741
|
+
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4742
|
+
if (!existsSync2(resolved)) {
|
|
4743
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4769
4744
|
}
|
|
4770
|
-
|
|
4771
|
-
return;
|
|
4772
|
-
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
4745
|
+
return { path: rel, content: readFileSync(resolved, "utf-8") };
|
|
4773
4746
|
}
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
const
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
configurable: false
|
|
4792
|
-
});
|
|
4793
|
-
if (fields.pointerVaultKey !== undefined) {
|
|
4794
|
-
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
4795
|
-
value: fields.pointerVaultKey,
|
|
4796
|
-
enumerable: false,
|
|
4797
|
-
writable: false,
|
|
4798
|
-
configurable: false
|
|
4799
|
-
});
|
|
4747
|
+
function customHookDir(name) {
|
|
4748
|
+
return join3(getCustomHooksDir(), shortManifestName(name));
|
|
4749
|
+
}
|
|
4750
|
+
function readCustomManifest(name) {
|
|
4751
|
+
const dir = customHookDir(name);
|
|
4752
|
+
const manifestPath = join3(dir, "manifest.json");
|
|
4753
|
+
if (!existsSync2(manifestPath))
|
|
4754
|
+
return;
|
|
4755
|
+
try {
|
|
4756
|
+
const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
|
|
4757
|
+
const script = resolveScript(manifest, dir);
|
|
4758
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4759
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4760
|
+
} catch (err) {
|
|
4761
|
+
if (err instanceof ScriptContainmentError)
|
|
4762
|
+
throw err;
|
|
4763
|
+
return;
|
|
4800
4764
|
}
|
|
4801
|
-
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4802
|
-
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4803
|
-
enumerable: false,
|
|
4804
|
-
writable: false,
|
|
4805
|
-
configurable: false
|
|
4806
|
-
});
|
|
4807
|
-
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4808
|
-
value: true,
|
|
4809
|
-
enumerable: false,
|
|
4810
|
-
writable: false,
|
|
4811
|
-
configurable: false
|
|
4812
|
-
});
|
|
4813
|
-
return Object.freeze(sealed);
|
|
4814
4765
|
}
|
|
4815
|
-
function
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4766
|
+
function listCustomHooks() {
|
|
4767
|
+
const dir = getCustomHooksDir();
|
|
4768
|
+
if (!existsSync2(dir))
|
|
4769
|
+
return [];
|
|
4770
|
+
const results = [];
|
|
4771
|
+
for (const entry of readdirSafe(dir)) {
|
|
4772
|
+
const parsed = readCustomManifest(entry);
|
|
4773
|
+
if (parsed)
|
|
4774
|
+
results.push(parsed);
|
|
4820
4775
|
}
|
|
4821
|
-
return
|
|
4776
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4822
4777
|
}
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
return existing;
|
|
4829
|
-
const created = new Set;
|
|
4830
|
-
host[DEPRECATION_REGISTRY] = created;
|
|
4831
|
-
return created;
|
|
4832
|
-
}
|
|
4833
|
-
function defaultDeprecationSink(message) {
|
|
4834
|
-
if (typeof process !== "undefined" && process.stderr) {
|
|
4835
|
-
process.stderr.write(`${message}
|
|
4836
|
-
`);
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
function resolveCredential(name, env, options = {}) {
|
|
4840
|
-
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
4841
|
-
const diskPaths = credentialDiskSources(name, env);
|
|
4842
|
-
const explicitKey = options.apiKey?.trim();
|
|
4843
|
-
if (explicitKey) {
|
|
4844
|
-
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4845
|
-
return sealCredential({
|
|
4846
|
-
apiKey: explicitKey,
|
|
4847
|
-
tier: "argument",
|
|
4848
|
-
source: "explicit apiKey argument",
|
|
4849
|
-
deliberate: true,
|
|
4850
|
-
deprecated: false,
|
|
4851
|
-
diskCandidates: diskPaths,
|
|
4852
|
-
warning: null
|
|
4853
|
-
});
|
|
4854
|
-
}
|
|
4855
|
-
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4856
|
-
const overrideRaw = env[overrideKeyName];
|
|
4857
|
-
if (overrideRaw !== undefined) {
|
|
4858
|
-
const override = overrideRaw.trim();
|
|
4859
|
-
if (!override) {
|
|
4860
|
-
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]);
|
|
4861
|
-
}
|
|
4862
|
-
assertUsableCredential(name, overrideKeyName, override);
|
|
4863
|
-
return sealCredential({
|
|
4864
|
-
apiKey: override,
|
|
4865
|
-
tier: "override",
|
|
4866
|
-
source: overrideKeyName,
|
|
4867
|
-
deliberate: true,
|
|
4868
|
-
deprecated: false,
|
|
4869
|
-
diskCandidates: diskPaths,
|
|
4870
|
-
warning: null
|
|
4871
|
-
});
|
|
4872
|
-
}
|
|
4873
|
-
const pointerKeyName = credentialPointerEnvKey(name);
|
|
4874
|
-
const pointerRaw = env[pointerKeyName];
|
|
4875
|
-
if (pointerRaw !== undefined) {
|
|
4876
|
-
const pointer = pointerRaw.trim();
|
|
4877
|
-
if (!pointer) {
|
|
4878
|
-
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
4879
|
-
}
|
|
4880
|
-
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
4881
|
-
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
4882
|
-
}
|
|
4883
|
-
return sealCredential({
|
|
4884
|
-
apiKey: "",
|
|
4885
|
-
pointerVaultKey: pointer,
|
|
4886
|
-
tier: "pointer",
|
|
4887
|
-
source: pointerKeyName,
|
|
4888
|
-
deliberate: true,
|
|
4889
|
-
deprecated: false,
|
|
4890
|
-
diskCandidates: diskPaths,
|
|
4891
|
-
warning: null
|
|
4892
|
-
});
|
|
4893
|
-
}
|
|
4894
|
-
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4895
|
-
if (profile) {
|
|
4896
|
-
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4897
|
-
if (!SAFE_PROFILE.test(profile)) {
|
|
4898
|
-
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4899
|
-
}
|
|
4900
|
-
const paths = profileDiskSources(name, env, profile);
|
|
4901
|
-
for (const path of paths) {
|
|
4902
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
4903
|
-
if (value) {
|
|
4904
|
-
assertUsableCredential(name, path, value);
|
|
4905
|
-
return sealCredential({
|
|
4906
|
-
apiKey: value,
|
|
4907
|
-
tier: "profile",
|
|
4908
|
-
source: path,
|
|
4909
|
-
deliberate: true,
|
|
4910
|
-
deprecated: false,
|
|
4911
|
-
diskCandidates: paths,
|
|
4912
|
-
warning: null
|
|
4913
|
-
});
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
4917
|
-
}
|
|
4918
|
-
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
4919
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4920
|
-
if (diskHits.length > 0) {
|
|
4921
|
-
const winner = diskHits[0];
|
|
4922
|
-
assertUsableCredential(name, winner.src.path, winner.value);
|
|
4923
|
-
const divergentSources = [
|
|
4924
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
4925
|
-
...(() => {
|
|
4926
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4927
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4928
|
-
})()
|
|
4929
|
-
];
|
|
4930
|
-
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
4931
|
-
let deprecated = winner.src.deprecated;
|
|
4932
|
-
let finalWarning = warning;
|
|
4933
|
-
if (winner.src.deprecated) {
|
|
4934
|
-
deprecated = true;
|
|
4935
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4936
|
-
const notified = deprecationNotified();
|
|
4937
|
-
const noticeKey = `${name}:${winner.src.path}`;
|
|
4938
|
-
if (!notified.has(noticeKey)) {
|
|
4939
|
-
notified.add(noticeKey);
|
|
4940
|
-
const target = diskSourceList[0]?.path ?? "<none>";
|
|
4941
|
-
const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
|
|
4942
|
-
sink(message);
|
|
4943
|
-
}
|
|
4944
|
-
finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
|
|
4945
|
-
}
|
|
4946
|
-
return sealCredential({
|
|
4947
|
-
apiKey: winner.value,
|
|
4948
|
-
tier: winner.src.tier,
|
|
4949
|
-
source: winner.src.path,
|
|
4950
|
-
deliberate: false,
|
|
4951
|
-
deprecated,
|
|
4952
|
-
diskCandidates: diskPaths,
|
|
4953
|
-
warning: finalWarning
|
|
4954
|
-
});
|
|
4955
|
-
}
|
|
4956
|
-
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
4957
|
-
if (legacy) {
|
|
4958
|
-
assertUsableCredential(name, legacy.key, legacy.value);
|
|
4959
|
-
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.`;
|
|
4960
|
-
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}`;
|
|
4961
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4962
|
-
const notified = deprecationNotified();
|
|
4963
|
-
if (!notified.has(name)) {
|
|
4964
|
-
notified.add(name);
|
|
4965
|
-
sink(message);
|
|
4966
|
-
}
|
|
4967
|
-
return sealCredential({
|
|
4968
|
-
apiKey: legacy.value,
|
|
4969
|
-
tier: "legacy-env",
|
|
4970
|
-
source: legacy.key,
|
|
4971
|
-
deliberate: false,
|
|
4972
|
-
deprecated: true,
|
|
4973
|
-
diskCandidates: diskPaths,
|
|
4974
|
-
warning: message
|
|
4975
|
-
});
|
|
4976
|
-
}
|
|
4977
|
-
return null;
|
|
4978
|
-
}
|
|
4979
|
-
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
4980
|
-
var requireSecretsSdk = createRequire(import.meta.url);
|
|
4981
|
-
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4982
|
-
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4983
|
-
"host",
|
|
4984
|
-
":authority",
|
|
4985
|
-
"forwarded",
|
|
4986
|
-
"x-forwarded-host",
|
|
4987
|
-
"x-original-host"
|
|
4988
|
-
]);
|
|
4989
|
-
|
|
4990
|
-
// src/config.ts
|
|
4991
|
-
import { join as join4 } from "path";
|
|
4992
|
-
function getHooksDataDir() {
|
|
4993
|
-
return getEffectiveDataRoot();
|
|
4994
|
-
}
|
|
4995
|
-
function getCustomHooksDir() {
|
|
4996
|
-
return join4(getHooksDataDir(), "hooks");
|
|
4997
|
-
}
|
|
4998
|
-
function getLockPath() {
|
|
4999
|
-
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
5000
|
-
if (explicit)
|
|
5001
|
-
return explicit;
|
|
5002
|
-
return join4(getHooksDataDir(), "hooks.lock");
|
|
5003
|
-
}
|
|
5004
|
-
function resolveApiKey() {
|
|
5005
|
-
return resolveCredential("hooks", process.env)?.apiKey;
|
|
5006
|
-
}
|
|
5007
|
-
|
|
5008
|
-
// src/lib/semver.ts
|
|
5009
|
-
var NUMERIC_ID = "(0|[1-9]\\d{0,15})";
|
|
5010
|
-
var ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*";
|
|
5011
|
-
var SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
5012
|
-
|
|
5013
|
-
// src/lib/manifest.ts
|
|
5014
|
-
var HOOK_NAME_RE = /^[\w-]+$/;
|
|
5015
|
-
var manifestSchema = exports_external.object({
|
|
5016
|
-
name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
5017
|
-
version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
5018
|
-
description: exports_external.string().optional(),
|
|
5019
|
-
events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
|
|
5020
|
-
script: exports_external.string().min(1, "script must name a relative path or inline content"),
|
|
5021
|
-
script_kind: exports_external.enum(["inline", "file"]).optional(),
|
|
5022
|
-
args: exports_external.array(exports_external.string()).optional(),
|
|
5023
|
-
timeout_ms: exports_external.number().int().positive().optional(),
|
|
5024
|
-
env: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
5025
|
-
});
|
|
5026
|
-
function parseManifest(json) {
|
|
5027
|
-
const parsed = JSON.parse(json);
|
|
5028
|
-
return manifestSchema.parse(parsed);
|
|
5029
|
-
}
|
|
5030
|
-
function shortManifestName(name) {
|
|
5031
|
-
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
5032
|
-
}
|
|
5033
|
-
|
|
5034
|
-
class ScriptContainmentError extends Error {
|
|
5035
|
-
constructor(path, hookDir, raw) {
|
|
5036
|
-
super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
|
|
5037
|
-
this.name = "ScriptContainmentError";
|
|
5038
|
-
}
|
|
5039
|
-
}
|
|
5040
|
-
function isWithin(child, parent) {
|
|
5041
|
-
return child === parent || child.startsWith(parent + sep);
|
|
5042
|
-
}
|
|
5043
|
-
function assertContainedLexical(target, root, raw) {
|
|
5044
|
-
const lexical = resolve2(target);
|
|
5045
|
-
const rootLexical = resolve2(root);
|
|
5046
|
-
if (!isWithin(lexical, rootLexical)) {
|
|
5047
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
5048
|
-
}
|
|
5049
|
-
return lexical;
|
|
5050
|
-
}
|
|
5051
|
-
function realpathNearest(path) {
|
|
5052
|
-
const tail = [];
|
|
5053
|
-
let cur = path;
|
|
5054
|
-
while (!existsSync2(cur)) {
|
|
5055
|
-
const parent = dirname(cur);
|
|
5056
|
-
if (parent === cur)
|
|
5057
|
-
break;
|
|
5058
|
-
tail.unshift(basename(cur));
|
|
5059
|
-
cur = parent;
|
|
5060
|
-
}
|
|
5061
|
-
return join5(realpathSync(cur), ...tail);
|
|
5062
|
-
}
|
|
5063
|
-
function assertContainedReal(root, target, raw) {
|
|
5064
|
-
const rootReal = realpathSync(root);
|
|
5065
|
-
const targetReal = realpathNearest(target);
|
|
5066
|
-
if (!isWithin(targetReal, rootReal)) {
|
|
5067
|
-
throw new ScriptContainmentError(target, root, raw);
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
function assertContained(target, root, raw) {
|
|
5071
|
-
const lexical = assertContainedLexical(target, root, raw);
|
|
5072
|
-
assertContainedReal(root, lexical, raw);
|
|
5073
|
-
return lexical;
|
|
5074
|
-
}
|
|
5075
|
-
function isInlineScript(manifest) {
|
|
5076
|
-
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
5077
|
-
`);
|
|
5078
|
-
}
|
|
5079
|
-
function scriptRelFor(manifest) {
|
|
5080
|
-
if (isInlineScript(manifest)) {
|
|
5081
|
-
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
5082
|
-
return `script${ext}`;
|
|
5083
|
-
}
|
|
5084
|
-
const rel = normalize(manifest.script);
|
|
5085
|
-
if (isAbsolute(rel)) {
|
|
5086
|
-
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
5087
|
-
}
|
|
5088
|
-
return rel;
|
|
5089
|
-
}
|
|
5090
|
-
function resolveScript(manifest, manifestDir) {
|
|
5091
|
-
if (isInlineScript(manifest))
|
|
5092
|
-
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
5093
|
-
const rel = scriptRelFor(manifest);
|
|
5094
|
-
const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
|
|
5095
|
-
if (!existsSync2(resolved)) {
|
|
5096
|
-
throw new Error(`manifest script file not found: ${resolved}`);
|
|
5097
|
-
}
|
|
5098
|
-
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
5099
|
-
}
|
|
5100
|
-
function customHookDir(name) {
|
|
5101
|
-
return join5(getCustomHooksDir(), shortManifestName(name));
|
|
5102
|
-
}
|
|
5103
|
-
function readCustomManifest(name) {
|
|
5104
|
-
const dir = customHookDir(name);
|
|
5105
|
-
const manifestPath = join5(dir, "manifest.json");
|
|
5106
|
-
if (!existsSync2(manifestPath))
|
|
5107
|
-
return;
|
|
5108
|
-
try {
|
|
5109
|
-
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
5110
|
-
const script = resolveScript(manifest, dir);
|
|
5111
|
-
const scriptIsInline = isInlineScript(manifest);
|
|
5112
|
-
return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
5113
|
-
} catch (err) {
|
|
5114
|
-
if (err instanceof ScriptContainmentError)
|
|
5115
|
-
throw err;
|
|
5116
|
-
return;
|
|
5117
|
-
}
|
|
5118
|
-
}
|
|
5119
|
-
function listCustomHooks() {
|
|
5120
|
-
const dir = getCustomHooksDir();
|
|
5121
|
-
if (!existsSync2(dir))
|
|
5122
|
-
return [];
|
|
5123
|
-
const results = [];
|
|
5124
|
-
for (const entry of readdirSafe(dir)) {
|
|
5125
|
-
const parsed = readCustomManifest(entry);
|
|
5126
|
-
if (parsed)
|
|
5127
|
-
results.push(parsed);
|
|
5128
|
-
}
|
|
5129
|
-
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
5130
|
-
}
|
|
5131
|
-
function readdirSafe(dir) {
|
|
5132
|
-
try {
|
|
5133
|
-
return readdirSync(dir);
|
|
5134
|
-
} catch {
|
|
5135
|
-
return [];
|
|
4778
|
+
function readdirSafe(dir) {
|
|
4779
|
+
try {
|
|
4780
|
+
return readdirSync(dir);
|
|
4781
|
+
} catch {
|
|
4782
|
+
return [];
|
|
5136
4783
|
}
|
|
5137
4784
|
}
|
|
5138
4785
|
|
|
5139
4786
|
// src/lib/store.ts
|
|
5140
4787
|
import { createHash } from "crypto";
|
|
5141
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as
|
|
4788
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5142
4789
|
import { readFile } from "fs/promises";
|
|
5143
4790
|
|
|
5144
4791
|
// src/db/index.ts
|
|
5145
4792
|
import { Database } from "bun:sqlite";
|
|
5146
4793
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
5147
|
-
import { join as
|
|
4794
|
+
import { join as join5 } from "path";
|
|
5148
4795
|
|
|
5149
4796
|
// src/db/schema.ts
|
|
5150
4797
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
@@ -5316,9 +4963,9 @@ function runMigrations(db) {
|
|
|
5316
4963
|
}
|
|
5317
4964
|
|
|
5318
4965
|
// src/db/legacy-import.ts
|
|
5319
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as
|
|
5320
|
-
import { join as
|
|
5321
|
-
import { homedir as
|
|
4966
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
4967
|
+
import { join as join4 } from "path";
|
|
4968
|
+
import { homedir as homedir2 } from "os";
|
|
5322
4969
|
var META_KEY = "legacy_import_done";
|
|
5323
4970
|
function ensureMetaTable(db) {
|
|
5324
4971
|
db.exec(`
|
|
@@ -5342,7 +4989,7 @@ function nanoid() {
|
|
|
5342
4989
|
function importJsonlFile(db, filePath) {
|
|
5343
4990
|
let count = 0;
|
|
5344
4991
|
try {
|
|
5345
|
-
const lines =
|
|
4992
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5346
4993
|
`).filter(Boolean);
|
|
5347
4994
|
for (const line of lines) {
|
|
5348
4995
|
try {
|
|
@@ -5368,7 +5015,7 @@ function importJsonlFile(db, filePath) {
|
|
|
5368
5015
|
function importErrorsLog(db, filePath) {
|
|
5369
5016
|
let count = 0;
|
|
5370
5017
|
try {
|
|
5371
|
-
const lines =
|
|
5018
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5372
5019
|
`).filter(Boolean);
|
|
5373
5020
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
5374
5021
|
for (const line of lines) {
|
|
@@ -5393,25 +5040,25 @@ function importErrorsLog(db, filePath) {
|
|
|
5393
5040
|
} catch {}
|
|
5394
5041
|
return count;
|
|
5395
5042
|
}
|
|
5396
|
-
function runLegacyImport(db,
|
|
5043
|
+
function runLegacyImport(db, homeDir = homedir2()) {
|
|
5397
5044
|
try {
|
|
5398
5045
|
if (isAlreadyDone(db))
|
|
5399
5046
|
return;
|
|
5400
5047
|
let total = 0;
|
|
5401
|
-
const claudeProjectsDir =
|
|
5048
|
+
const claudeProjectsDir = join4(homeDir, ".claude", "projects");
|
|
5402
5049
|
if (existsSync3(claudeProjectsDir)) {
|
|
5403
5050
|
try {
|
|
5404
5051
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
5405
5052
|
for (const dir of projectDirs) {
|
|
5406
|
-
const projectDir =
|
|
5053
|
+
const projectDir = join4(claudeProjectsDir, dir);
|
|
5407
5054
|
try {
|
|
5408
5055
|
const files = readdirSync2(projectDir);
|
|
5409
5056
|
for (const file of files) {
|
|
5410
5057
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
5411
|
-
total += importJsonlFile(db,
|
|
5058
|
+
total += importJsonlFile(db, join4(projectDir, file));
|
|
5412
5059
|
}
|
|
5413
5060
|
if (file === "errors.log") {
|
|
5414
|
-
total += importErrorsLog(db,
|
|
5061
|
+
total += importErrorsLog(db, join4(projectDir, file));
|
|
5415
5062
|
}
|
|
5416
5063
|
}
|
|
5417
5064
|
} catch {}
|
|
@@ -5447,7 +5094,7 @@ function runRetention(db, days) {
|
|
|
5447
5094
|
var instance = null;
|
|
5448
5095
|
function resolveDataDir() {
|
|
5449
5096
|
const effective = getEffectiveDataRoot();
|
|
5450
|
-
const oldDir =
|
|
5097
|
+
const oldDir = join5(getHomeDir(), ".hooks");
|
|
5451
5098
|
if (!existsSync4(effective) && existsSync4(oldDir)) {
|
|
5452
5099
|
mkdirSync2(effective, { recursive: true });
|
|
5453
5100
|
cpSync(oldDir, effective, { recursive: true });
|
|
@@ -5459,7 +5106,7 @@ function getDbPath() {
|
|
|
5459
5106
|
if (explicitDb)
|
|
5460
5107
|
return explicitDb;
|
|
5461
5108
|
const dataDir2 = resolveDataDir();
|
|
5462
|
-
return
|
|
5109
|
+
return join5(dataDir2, "hooks.db");
|
|
5463
5110
|
}
|
|
5464
5111
|
function ensureDir(dbPath) {
|
|
5465
5112
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
@@ -5558,7 +5205,7 @@ function readLock() {
|
|
|
5558
5205
|
return { hooks: {} };
|
|
5559
5206
|
let raw;
|
|
5560
5207
|
try {
|
|
5561
|
-
raw =
|
|
5208
|
+
raw = readFileSync3(path, "utf-8");
|
|
5562
5209
|
} catch {
|
|
5563
5210
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5564
5211
|
}
|
|
@@ -5590,7 +5237,7 @@ function setPinnedHook(name, entry) {
|
|
|
5590
5237
|
return writeLock(lock);
|
|
5591
5238
|
}
|
|
5592
5239
|
function retrustHook(name, scriptPath, version, source) {
|
|
5593
|
-
const actual = sha256Of(
|
|
5240
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5594
5241
|
const db = getDb();
|
|
5595
5242
|
upsertHookRecord(db, {
|
|
5596
5243
|
name,
|
|
@@ -5604,14 +5251,14 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
5604
5251
|
}
|
|
5605
5252
|
|
|
5606
5253
|
// src/lib/resolve.ts
|
|
5607
|
-
import { dirname as dirname2, join as
|
|
5254
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
5608
5255
|
import { existsSync as existsSync6 } from "fs";
|
|
5609
5256
|
import { fileURLToPath } from "url";
|
|
5610
5257
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
5611
|
-
const sourceLayout =
|
|
5612
|
-
if (existsSync6(
|
|
5258
|
+
const sourceLayout = join6(moduleDir, "..", "..", "hooks");
|
|
5259
|
+
if (existsSync6(join6(sourceLayout, "hook-gitguard")))
|
|
5613
5260
|
return sourceLayout;
|
|
5614
|
-
return
|
|
5261
|
+
return join6(moduleDir, "..", "hooks");
|
|
5615
5262
|
}
|
|
5616
5263
|
function resolveHookDir(name) {
|
|
5617
5264
|
const custom2 = readCustomManifest(name);
|
|
@@ -5622,10 +5269,10 @@ function resolveHookDir(name) {
|
|
|
5622
5269
|
return;
|
|
5623
5270
|
const short = shortManifestName(name);
|
|
5624
5271
|
const bundledBase = resolveBundledHooksDir();
|
|
5625
|
-
const direct =
|
|
5272
|
+
const direct = join6(bundledBase, short);
|
|
5626
5273
|
if (existsSync6(direct))
|
|
5627
5274
|
return direct;
|
|
5628
|
-
const prefixed =
|
|
5275
|
+
const prefixed = join6(bundledBase, `hook-${short}`);
|
|
5629
5276
|
return existsSync6(prefixed) ? prefixed : undefined;
|
|
5630
5277
|
}
|
|
5631
5278
|
function resolveScriptPath(name) {
|
|
@@ -5635,7 +5282,7 @@ function resolveScriptPath(name) {
|
|
|
5635
5282
|
const dir = resolveHookDir(name);
|
|
5636
5283
|
if (!dir)
|
|
5637
5284
|
return;
|
|
5638
|
-
const script =
|
|
5285
|
+
const script = join6(dir, "src", "hook.ts");
|
|
5639
5286
|
return existsSync6(script) ? script : undefined;
|
|
5640
5287
|
}
|
|
5641
5288
|
function resolveHookMeta(name) {
|
|
@@ -5689,9 +5336,554 @@ function resolveHook(name) {
|
|
|
5689
5336
|
|
|
5690
5337
|
// src/serve.ts
|
|
5691
5338
|
import { readFileSync as readFileSync5 } from "fs";
|
|
5692
|
-
import { dirname as dirname3, join as
|
|
5339
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
5693
5340
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5694
5341
|
|
|
5342
|
+
// ../contracts/dist/client/transport.js
|
|
5343
|
+
import { spawnSync } from "child_process";
|
|
5344
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync4 } from "fs";
|
|
5345
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5346
|
+
import { createRequire } from "module";
|
|
5347
|
+
import { hostname as osHostname } from "os";
|
|
5348
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
5349
|
+
function envToken(name) {
|
|
5350
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
5351
|
+
}
|
|
5352
|
+
function clientTransportEnvKeys(name) {
|
|
5353
|
+
const envSegment = envToken(name);
|
|
5354
|
+
return {
|
|
5355
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
5356
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
function credentialOverrideEnvKey(name) {
|
|
5360
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
5361
|
+
}
|
|
5362
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
5363
|
+
function credentialPointerEnvKey(name) {
|
|
5364
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
class CredentialResolutionError extends Error {
|
|
5368
|
+
appName;
|
|
5369
|
+
attempted;
|
|
5370
|
+
constructor(appName, message, attempted) {
|
|
5371
|
+
super(message);
|
|
5372
|
+
this.name = "CredentialResolutionError";
|
|
5373
|
+
this.appName = appName;
|
|
5374
|
+
this.attempted = attempted;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
class CredentialFileUnsafeError extends Error {
|
|
5379
|
+
path;
|
|
5380
|
+
constructor(path, reason) {
|
|
5381
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
5382
|
+
this.name = "CredentialFileUnsafeError";
|
|
5383
|
+
this.path = path;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
5387
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
5388
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
5389
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
5390
|
+
var CONFIG_SUBDIR = "config";
|
|
5391
|
+
var CREDENTIALS_FILE = "credentials";
|
|
5392
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
5393
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
5394
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
5395
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
5396
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
5397
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
5398
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
5399
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
5400
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
5401
|
+
function homeDir(env) {
|
|
5402
|
+
const home = env.HOME?.trim();
|
|
5403
|
+
return home ? home : null;
|
|
5404
|
+
}
|
|
5405
|
+
function absoluteOverride(env, key) {
|
|
5406
|
+
const value = env[key]?.trim();
|
|
5407
|
+
return value && isAbsolute2(value) ? value : null;
|
|
5408
|
+
}
|
|
5409
|
+
function hasnaHomeDir(env) {
|
|
5410
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
5411
|
+
if (override)
|
|
5412
|
+
return override;
|
|
5413
|
+
const home = homeDir(env);
|
|
5414
|
+
return home ? join7(home, HASNA_HOME_DIR) : null;
|
|
5415
|
+
}
|
|
5416
|
+
function appConfigDir(name, env) {
|
|
5417
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
5418
|
+
if (configRoot)
|
|
5419
|
+
return join7(configRoot, name);
|
|
5420
|
+
const root = hasnaHomeDir(env);
|
|
5421
|
+
return root ? join7(root, name, CONFIG_SUBDIR) : null;
|
|
5422
|
+
}
|
|
5423
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
5424
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
5425
|
+
return [];
|
|
5426
|
+
const directory = appConfigDir(name, env);
|
|
5427
|
+
if (!directory)
|
|
5428
|
+
return [];
|
|
5429
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
5430
|
+
return [{ path: join7(directory, file), tier: "disk" }];
|
|
5431
|
+
}
|
|
5432
|
+
function credentialDiskSources(name, env) {
|
|
5433
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
5434
|
+
}
|
|
5435
|
+
function profileDiskSources(name, env, profile) {
|
|
5436
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
5437
|
+
}
|
|
5438
|
+
function parseEnvFile(text) {
|
|
5439
|
+
const values = new Map;
|
|
5440
|
+
const unusable = new Set;
|
|
5441
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5442
|
+
const line = rawLine.trim();
|
|
5443
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
5444
|
+
continue;
|
|
5445
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
5446
|
+
const equals = withoutExport.indexOf("=");
|
|
5447
|
+
if (equals <= 0)
|
|
5448
|
+
continue;
|
|
5449
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
5450
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5451
|
+
continue;
|
|
5452
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
5453
|
+
const quote = value[0];
|
|
5454
|
+
if (quote === '"' || quote === "'") {
|
|
5455
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
5456
|
+
unusable.add(key);
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
5459
|
+
value = value.slice(1, -1);
|
|
5460
|
+
}
|
|
5461
|
+
if (value.trim().length === 0) {
|
|
5462
|
+
unusable.add(key);
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (values.has(key) && values.get(key) !== value)
|
|
5466
|
+
unusable.add(key);
|
|
5467
|
+
values.set(key, value);
|
|
5468
|
+
}
|
|
5469
|
+
return { values, unusable };
|
|
5470
|
+
}
|
|
5471
|
+
function configFileModeAllowed(mode) {
|
|
5472
|
+
const permissions = mode & 4095;
|
|
5473
|
+
return permissions === 256 || permissions === 384;
|
|
5474
|
+
}
|
|
5475
|
+
function configFileReadsCoherent(before, after) {
|
|
5476
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
5477
|
+
}
|
|
5478
|
+
function readAppConfigFile(path) {
|
|
5479
|
+
const unsafe = (reason) => {
|
|
5480
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
5481
|
+
};
|
|
5482
|
+
let fd = -1;
|
|
5483
|
+
try {
|
|
5484
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
5485
|
+
} catch (error) {
|
|
5486
|
+
const code = error.code;
|
|
5487
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
5488
|
+
return null;
|
|
5489
|
+
if (code === "ELOOP")
|
|
5490
|
+
unsafe("the path is a symlink");
|
|
5491
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
5492
|
+
}
|
|
5493
|
+
try {
|
|
5494
|
+
const before = fstatSync(fd);
|
|
5495
|
+
if (!before.isFile())
|
|
5496
|
+
unsafe("the path is not a regular file");
|
|
5497
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
5498
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
5499
|
+
}
|
|
5500
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
5501
|
+
if (uid !== undefined && before.uid !== uid)
|
|
5502
|
+
unsafe("the file is not owned by the current user");
|
|
5503
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
5504
|
+
unsafe("the file exceeds the size limit");
|
|
5505
|
+
const bytes = readFileSync4(fd);
|
|
5506
|
+
const after = fstatSync(fd);
|
|
5507
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
5508
|
+
unsafe("the file changed while being read");
|
|
5509
|
+
}
|
|
5510
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
5511
|
+
} finally {
|
|
5512
|
+
if (fd !== -1)
|
|
5513
|
+
closeSync(fd);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
5517
|
+
const parsed = readAppConfigFile(path);
|
|
5518
|
+
if (!parsed)
|
|
5519
|
+
return null;
|
|
5520
|
+
for (const key of apiKeyKeys) {
|
|
5521
|
+
if (parsed.unusable.has(key)) {
|
|
5522
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
5526
|
+
if (new Set(values).size > 1) {
|
|
5527
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
5528
|
+
}
|
|
5529
|
+
return values[0] ?? null;
|
|
5530
|
+
}
|
|
5531
|
+
function assertUsableCredential(appName, source, value) {
|
|
5532
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
5533
|
+
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]);
|
|
5534
|
+
}
|
|
5535
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
5536
|
+
return;
|
|
5537
|
+
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]);
|
|
5538
|
+
}
|
|
5539
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
5540
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
5541
|
+
function sealCredential(fields) {
|
|
5542
|
+
const { apiKey } = fields;
|
|
5543
|
+
const visible = {
|
|
5544
|
+
tier: fields.tier,
|
|
5545
|
+
source: fields.source,
|
|
5546
|
+
deliberate: fields.deliberate,
|
|
5547
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
5548
|
+
warning: fields.warning
|
|
5549
|
+
};
|
|
5550
|
+
const sealed = { ...visible };
|
|
5551
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
5552
|
+
value: apiKey,
|
|
5553
|
+
enumerable: false,
|
|
5554
|
+
writable: false,
|
|
5555
|
+
configurable: false
|
|
5556
|
+
});
|
|
5557
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
5558
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
5559
|
+
value: fields.pointerVaultKey,
|
|
5560
|
+
enumerable: false,
|
|
5561
|
+
writable: false,
|
|
5562
|
+
configurable: false
|
|
5563
|
+
});
|
|
5564
|
+
}
|
|
5565
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
5566
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
5567
|
+
enumerable: false,
|
|
5568
|
+
writable: false,
|
|
5569
|
+
configurable: false
|
|
5570
|
+
});
|
|
5571
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
5572
|
+
value: true,
|
|
5573
|
+
enumerable: false,
|
|
5574
|
+
writable: false,
|
|
5575
|
+
configurable: false
|
|
5576
|
+
});
|
|
5577
|
+
return Object.freeze(sealed);
|
|
5578
|
+
}
|
|
5579
|
+
function firstEnvValue(env, keys) {
|
|
5580
|
+
for (const key of keys) {
|
|
5581
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
5582
|
+
continue;
|
|
5583
|
+
const value = env[key]?.trim();
|
|
5584
|
+
if (value)
|
|
5585
|
+
return { key, value };
|
|
5586
|
+
}
|
|
5587
|
+
return null;
|
|
5588
|
+
}
|
|
5589
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
5590
|
+
function isAmbientEnvironment(env) {
|
|
5591
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
5592
|
+
}
|
|
5593
|
+
function defaultKeychainRunner(argv) {
|
|
5594
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
5595
|
+
encoding: "utf8",
|
|
5596
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5597
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
5598
|
+
});
|
|
5599
|
+
return {
|
|
5600
|
+
status: result.status,
|
|
5601
|
+
stdout: result.stdout ?? "",
|
|
5602
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
5603
|
+
};
|
|
5604
|
+
}
|
|
5605
|
+
function keychainTierEnabled(env, options) {
|
|
5606
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
5607
|
+
return false;
|
|
5608
|
+
if (options.enabled !== undefined)
|
|
5609
|
+
return options.enabled;
|
|
5610
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
5611
|
+
}
|
|
5612
|
+
function keychainAccount(env, options) {
|
|
5613
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
5614
|
+
if (station)
|
|
5615
|
+
return station;
|
|
5616
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
5617
|
+
if (host)
|
|
5618
|
+
return host;
|
|
5619
|
+
const user = env.USER?.trim();
|
|
5620
|
+
return user || null;
|
|
5621
|
+
}
|
|
5622
|
+
function keychainFailureHint(text) {
|
|
5623
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
5624
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
5625
|
+
return clean ? `: ${clean}` : "";
|
|
5626
|
+
}
|
|
5627
|
+
function readKeychainItem(name, env, kind, options) {
|
|
5628
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
5629
|
+
return null;
|
|
5630
|
+
const account = keychainAccount(env, options);
|
|
5631
|
+
if (!account)
|
|
5632
|
+
return null;
|
|
5633
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
5634
|
+
const source = `keychain:${service}@${account}`;
|
|
5635
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
5636
|
+
let result;
|
|
5637
|
+
try {
|
|
5638
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
5639
|
+
} catch (error) {
|
|
5640
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
5641
|
+
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]);
|
|
5642
|
+
}
|
|
5643
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
5644
|
+
return null;
|
|
5645
|
+
if (result.status !== 0) {
|
|
5646
|
+
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]);
|
|
5647
|
+
}
|
|
5648
|
+
const value = result.stdout.trim();
|
|
5649
|
+
if (!value) {
|
|
5650
|
+
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]);
|
|
5651
|
+
}
|
|
5652
|
+
return { value, source };
|
|
5653
|
+
}
|
|
5654
|
+
function snapshotClientEnvironment(name, env) {
|
|
5655
|
+
const keys = clientTransportEnvKeys(name);
|
|
5656
|
+
const ambient = isAmbientEnvironment(env);
|
|
5657
|
+
const snapshot = Object.create(null);
|
|
5658
|
+
for (const key of [
|
|
5659
|
+
...keys.apiUrlKeys,
|
|
5660
|
+
...keys.apiKeyKeys,
|
|
5661
|
+
credentialOverrideEnvKey(name),
|
|
5662
|
+
credentialPointerEnvKey(name),
|
|
5663
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
5664
|
+
"HOME",
|
|
5665
|
+
HASNA_HOME_ENV_KEY,
|
|
5666
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
5667
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
5668
|
+
"USER"
|
|
5669
|
+
]) {
|
|
5670
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
5671
|
+
if (!descriptor)
|
|
5672
|
+
continue;
|
|
5673
|
+
if (!("value" in descriptor)) {
|
|
5674
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
5675
|
+
}
|
|
5676
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
5677
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
5678
|
+
}
|
|
5679
|
+
snapshot[key] = descriptor.value;
|
|
5680
|
+
}
|
|
5681
|
+
if (ambient) {
|
|
5682
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
5683
|
+
value: true,
|
|
5684
|
+
enumerable: false,
|
|
5685
|
+
writable: false,
|
|
5686
|
+
configurable: false
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
return Object.freeze(snapshot);
|
|
5690
|
+
}
|
|
5691
|
+
function resolveCredential(name, env, options = {}) {
|
|
5692
|
+
env = snapshotClientEnvironment(name, env);
|
|
5693
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
5694
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
5695
|
+
if (options.apiKey !== undefined) {
|
|
5696
|
+
const explicitKey = options.apiKey.trim();
|
|
5697
|
+
if (!explicitKey) {
|
|
5698
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
5699
|
+
}
|
|
5700
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
5701
|
+
return sealCredential({
|
|
5702
|
+
apiKey: explicitKey,
|
|
5703
|
+
tier: "argument",
|
|
5704
|
+
source: "explicit apiKey argument",
|
|
5705
|
+
deliberate: true,
|
|
5706
|
+
diskCandidates: diskPaths,
|
|
5707
|
+
warning: null
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5710
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
5711
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
5712
|
+
if (overrideRaw !== undefined) {
|
|
5713
|
+
const override = overrideRaw.trim();
|
|
5714
|
+
if (!override) {
|
|
5715
|
+
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]);
|
|
5716
|
+
}
|
|
5717
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
5718
|
+
return sealCredential({
|
|
5719
|
+
apiKey: override,
|
|
5720
|
+
tier: "override",
|
|
5721
|
+
source: overrideKeyName,
|
|
5722
|
+
deliberate: true,
|
|
5723
|
+
diskCandidates: diskPaths,
|
|
5724
|
+
warning: null
|
|
5725
|
+
});
|
|
5726
|
+
}
|
|
5727
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
5728
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
5729
|
+
if (pointerRaw !== undefined) {
|
|
5730
|
+
const pointer = pointerRaw.trim();
|
|
5731
|
+
if (!pointer) {
|
|
5732
|
+
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]);
|
|
5733
|
+
}
|
|
5734
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
5735
|
+
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]);
|
|
5736
|
+
}
|
|
5737
|
+
return sealCredential({
|
|
5738
|
+
apiKey: "",
|
|
5739
|
+
pointerVaultKey: pointer,
|
|
5740
|
+
tier: "pointer",
|
|
5741
|
+
source: pointerKeyName,
|
|
5742
|
+
deliberate: true,
|
|
5743
|
+
diskCandidates: diskPaths,
|
|
5744
|
+
warning: null
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
5747
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
5748
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
5749
|
+
}
|
|
5750
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
5751
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
5752
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
5753
|
+
}
|
|
5754
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
5755
|
+
if (profile) {
|
|
5756
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
5757
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
5758
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
5759
|
+
}
|
|
5760
|
+
const paths = profileDiskSources(name, env, profile);
|
|
5761
|
+
for (const path of paths) {
|
|
5762
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
5763
|
+
if (value) {
|
|
5764
|
+
assertUsableCredential(name, path, value);
|
|
5765
|
+
return sealCredential({
|
|
5766
|
+
apiKey: value,
|
|
5767
|
+
tier: "profile",
|
|
5768
|
+
source: path,
|
|
5769
|
+
deliberate: true,
|
|
5770
|
+
diskCandidates: paths,
|
|
5771
|
+
warning: null
|
|
5772
|
+
});
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
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);
|
|
5776
|
+
}
|
|
5777
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
5778
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
5779
|
+
if (blankEnv) {
|
|
5780
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
5781
|
+
}
|
|
5782
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
5783
|
+
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));
|
|
5784
|
+
}
|
|
5785
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
5786
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
5787
|
+
if (keychainHit) {
|
|
5788
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
5789
|
+
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;
|
|
5790
|
+
return sealCredential({
|
|
5791
|
+
apiKey: keychainHit.value,
|
|
5792
|
+
tier: "keychain",
|
|
5793
|
+
source: keychainHit.source,
|
|
5794
|
+
deliberate: false,
|
|
5795
|
+
diskCandidates: diskPaths,
|
|
5796
|
+
warning
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
5800
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
5801
|
+
if (diskHits.length > 0) {
|
|
5802
|
+
const winner = diskHits[0];
|
|
5803
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
5804
|
+
const divergentSources = [
|
|
5805
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
5806
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
5807
|
+
];
|
|
5808
|
+
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;
|
|
5809
|
+
return sealCredential({
|
|
5810
|
+
apiKey: winner.value,
|
|
5811
|
+
tier: winner.src.tier,
|
|
5812
|
+
source: winner.src.path,
|
|
5813
|
+
deliberate: false,
|
|
5814
|
+
diskCandidates: diskPaths,
|
|
5815
|
+
warning
|
|
5816
|
+
});
|
|
5817
|
+
}
|
|
5818
|
+
if (envHit) {
|
|
5819
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
5820
|
+
return sealCredential({
|
|
5821
|
+
apiKey: envHit.value,
|
|
5822
|
+
tier: "env",
|
|
5823
|
+
source: envHit.key,
|
|
5824
|
+
deliberate: false,
|
|
5825
|
+
diskCandidates: diskPaths,
|
|
5826
|
+
warning: null
|
|
5827
|
+
});
|
|
5828
|
+
}
|
|
5829
|
+
return null;
|
|
5830
|
+
}
|
|
5831
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
5832
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
5833
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
5834
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
5835
|
+
"host",
|
|
5836
|
+
":authority",
|
|
5837
|
+
"forwarded",
|
|
5838
|
+
"x-forwarded-host",
|
|
5839
|
+
"x-original-host"
|
|
5840
|
+
]);
|
|
5841
|
+
|
|
5842
|
+
// src/lib/local-opt-in.ts
|
|
5843
|
+
function hooksAuthorityEnvKeys() {
|
|
5844
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
5845
|
+
return [
|
|
5846
|
+
...keys.apiUrlKeys,
|
|
5847
|
+
...keys.apiKeyKeys,
|
|
5848
|
+
credentialOverrideEnvKey("hooks"),
|
|
5849
|
+
credentialPointerEnvKey("hooks"),
|
|
5850
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
5851
|
+
];
|
|
5852
|
+
}
|
|
5853
|
+
function hooksResolverEnv(env) {
|
|
5854
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
5855
|
+
if (blanks.length === 0)
|
|
5856
|
+
return env;
|
|
5857
|
+
const next = { ...env };
|
|
5858
|
+
for (const key of blanks)
|
|
5859
|
+
delete next[key];
|
|
5860
|
+
return next;
|
|
5861
|
+
}
|
|
5862
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
5863
|
+
function isAmbientHooksEnv(env) {
|
|
5864
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
5865
|
+
return true;
|
|
5866
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
5867
|
+
}
|
|
5868
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
5869
|
+
const normalised = hooksResolverEnv(env);
|
|
5870
|
+
if (normalised === env)
|
|
5871
|
+
return { env: normalised, credentials };
|
|
5872
|
+
const keychain = { ...credentials.keychain };
|
|
5873
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
5874
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
5875
|
+
}
|
|
5876
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
// src/lib/transport.ts
|
|
5880
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
5881
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
5882
|
+
const tier1 = options.credentials?.apiKey;
|
|
5883
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
5884
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5695
5887
|
// src/lib/secure-compare.ts
|
|
5696
5888
|
function secureEqual(a, b) {
|
|
5697
5889
|
const enc = new TextEncoder;
|
|
@@ -5850,7 +6042,7 @@ var openApiDocument = {
|
|
|
5850
6042
|
apiKey: {
|
|
5851
6043
|
type: "http",
|
|
5852
6044
|
scheme: "bearer",
|
|
5853
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
6045
|
+
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."
|
|
5854
6046
|
}
|
|
5855
6047
|
}
|
|
5856
6048
|
}
|
|
@@ -5862,7 +6054,7 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
5862
6054
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
5863
6055
|
function packageVersion() {
|
|
5864
6056
|
try {
|
|
5865
|
-
const pkgPath =
|
|
6057
|
+
const pkgPath = join8(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
5866
6058
|
return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
|
|
5867
6059
|
} catch {
|
|
5868
6060
|
return "0.0.0";
|
|
@@ -6020,12 +6212,11 @@ function resolveServeOptions(options) {
|
|
|
6020
6212
|
}
|
|
6021
6213
|
function startServeServer(options) {
|
|
6022
6214
|
const { port, host } = resolveServeOptions(options);
|
|
6023
|
-
const apiKey = resolveApiKey();
|
|
6024
6215
|
const server = Bun.serve({
|
|
6025
6216
|
hostname: host,
|
|
6026
6217
|
port,
|
|
6027
6218
|
async fetch(req) {
|
|
6028
|
-
return handleServeRequest(req,
|
|
6219
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
6029
6220
|
}
|
|
6030
6221
|
});
|
|
6031
6222
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|