@hasna/hooks 0.7.10 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -6
- package/bin/index.js +1872 -1300
- package/bin/serve.js +730 -551
- package/dist/config.d.ts +25 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1198 -712
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/installer.d.ts +0 -9
- package/dist/lib/local-opt-in.d.ts +80 -0
- package/dist/lib/profiles.d.ts +10 -0
- package/dist/lib/resolver-types.d.ts +45 -0
- package/dist/lib/sync.d.ts +13 -8
- package/dist/lib/transport.d.ts +87 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/storage.js +54 -63
- package/hooks/hook-affected-tests/package.json +1 -1
- package/hooks/hook-agent-rules-version-check/package.json +1 -1
- package/hooks/hook-agentmessages/package.json +1 -1
- package/hooks/hook-announce-start/package.json +1 -1
- package/hooks/hook-announce-stop/package.json +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-branchprotect/package.json +1 -1
- package/hooks/hook-checkbugs/package.json +1 -1
- package/hooks/hook-checkdocs/package.json +1 -1
- package/hooks/hook-checkfiles/package.json +1 -1
- package/hooks/hook-checklint/package.json +1 -1
- package/hooks/hook-checkpoint/package.json +1 -1
- package/hooks/hook-checksecurity/package.json +1 -1
- package/hooks/hook-checktasks/package.json +1 -1
- package/hooks/hook-checktests/package.json +1 -1
- package/hooks/hook-conflict-detect/package.json +1 -1
- package/hooks/hook-contextrefresh/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-dm-inject/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-failure-to-task/package.json +1 -1
- package/hooks/hook-filelock/package.json +1 -1
- package/hooks/hook-fleet-blockers-gate/package.json +1 -1
- package/hooks/hook-fleet-catchup/package.json +1 -1
- package/hooks/hook-gitguard/package.json +1 -1
- package/hooks/hook-packageage/package.json +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-phonenotify/package.json +1 -1
- package/hooks/hook-precompact/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-stylescheck/package.json +1 -1
- package/hooks/hook-typecheck-gate/package.json +1 -1
- package/package.json +4 -9
- package/scripts/ensure-profiles-dir.mjs +52 -1
package/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;
|
|
@@ -656,8 +649,8 @@ function getHook(name) {
|
|
|
656
649
|
}
|
|
657
650
|
|
|
658
651
|
// src/lib/manifest.ts
|
|
659
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync
|
|
660
|
-
import { basename, dirname, isAbsolute, join as
|
|
652
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
653
|
+
import { basename, dirname, isAbsolute, join as join3, normalize, resolve as resolve2, sep } from "path";
|
|
661
654
|
|
|
662
655
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
663
656
|
var exports_external = {};
|
|
@@ -4632,519 +4625,161 @@ var coerce = {
|
|
|
4632
4625
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
4633
4626
|
};
|
|
4634
4627
|
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
|
-
};
|
|
4628
|
+
// src/config.ts
|
|
4629
|
+
import { join as join2 } from "path";
|
|
4630
|
+
function getHooksDataDir() {
|
|
4631
|
+
return getEffectiveDataRoot();
|
|
4648
4632
|
}
|
|
4649
|
-
function
|
|
4650
|
-
return
|
|
4633
|
+
function getCustomHooksDir() {
|
|
4634
|
+
return join2(getHooksDataDir(), "hooks");
|
|
4651
4635
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4636
|
+
function getLockPath() {
|
|
4637
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4638
|
+
if (explicit)
|
|
4639
|
+
return explicit;
|
|
4640
|
+
return join2(getHooksDataDir(), "hooks.lock");
|
|
4655
4641
|
}
|
|
4656
4642
|
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4643
|
+
// src/lib/semver.ts
|
|
4644
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})";
|
|
4645
|
+
var ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*";
|
|
4646
|
+
var SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4647
|
+
|
|
4648
|
+
// src/lib/manifest.ts
|
|
4649
|
+
var HOOK_NAME_RE = /^[\w-]+$/;
|
|
4650
|
+
var manifestSchema = exports_external.object({
|
|
4651
|
+
name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4652
|
+
version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4653
|
+
description: exports_external.string().optional(),
|
|
4654
|
+
events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
|
|
4655
|
+
script: exports_external.string().min(1, "script must name a relative path or inline content"),
|
|
4656
|
+
script_kind: exports_external.enum(["inline", "file"]).optional(),
|
|
4657
|
+
args: exports_external.array(exports_external.string()).optional(),
|
|
4658
|
+
timeout_ms: exports_external.number().int().positive().optional(),
|
|
4659
|
+
env: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
4660
|
+
});
|
|
4661
|
+
function parseManifest(json) {
|
|
4662
|
+
const parsed = JSON.parse(json);
|
|
4663
|
+
return manifestSchema.parse(parsed);
|
|
4666
4664
|
}
|
|
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;
|
|
4665
|
+
function shortManifestName(name) {
|
|
4666
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4681
4667
|
}
|
|
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
|
-
];
|
|
4668
|
+
|
|
4669
|
+
class ScriptContainmentError extends Error {
|
|
4670
|
+
constructor(path, hookDir, raw) {
|
|
4671
|
+
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}'`);
|
|
4672
|
+
this.name = "ScriptContainmentError";
|
|
4673
|
+
}
|
|
4710
4674
|
}
|
|
4711
|
-
function
|
|
4712
|
-
return
|
|
4675
|
+
function isWithin(child, parent) {
|
|
4676
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4713
4677
|
}
|
|
4714
|
-
function
|
|
4715
|
-
|
|
4678
|
+
function assertContainedLexical(target, root, raw) {
|
|
4679
|
+
const lexical = resolve2(target);
|
|
4680
|
+
const rootLexical = resolve2(root);
|
|
4681
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4682
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4683
|
+
}
|
|
4684
|
+
return lexical;
|
|
4716
4685
|
}
|
|
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);
|
|
4686
|
+
function realpathNearest(path) {
|
|
4687
|
+
const tail = [];
|
|
4688
|
+
let cur = path;
|
|
4689
|
+
while (!existsSync2(cur)) {
|
|
4690
|
+
const parent = dirname(cur);
|
|
4691
|
+
if (parent === cur)
|
|
4692
|
+
break;
|
|
4693
|
+
tail.unshift(basename(cur));
|
|
4694
|
+
cur = parent;
|
|
4740
4695
|
}
|
|
4741
|
-
return
|
|
4696
|
+
return join3(realpathSync(cur), ...tail);
|
|
4742
4697
|
}
|
|
4743
|
-
function
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
return null;
|
|
4749
|
-
text = readFileSync(path, "utf8");
|
|
4750
|
-
} catch {
|
|
4751
|
-
return null;
|
|
4698
|
+
function assertContainedReal(root, target, raw) {
|
|
4699
|
+
const rootReal = realpathSync(root);
|
|
4700
|
+
const targetReal = realpathNearest(target);
|
|
4701
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4702
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4752
4703
|
}
|
|
4753
|
-
return parseEnvFile(text);
|
|
4754
4704
|
}
|
|
4755
|
-
function
|
|
4756
|
-
const
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4705
|
+
function assertContained(target, root, raw) {
|
|
4706
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4707
|
+
assertContainedReal(root, lexical, raw);
|
|
4708
|
+
return lexical;
|
|
4709
|
+
}
|
|
4710
|
+
function isInlineScript(manifest) {
|
|
4711
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4712
|
+
`);
|
|
4713
|
+
}
|
|
4714
|
+
function scriptRelFor(manifest) {
|
|
4715
|
+
if (isInlineScript(manifest)) {
|
|
4716
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4717
|
+
return `script${ext}`;
|
|
4763
4718
|
}
|
|
4764
|
-
|
|
4719
|
+
const rel = normalize(manifest.script);
|
|
4720
|
+
if (isAbsolute(rel)) {
|
|
4721
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4722
|
+
}
|
|
4723
|
+
return rel;
|
|
4765
4724
|
}
|
|
4766
|
-
function
|
|
4767
|
-
if (
|
|
4768
|
-
|
|
4725
|
+
function resolveScript(manifest, manifestDir) {
|
|
4726
|
+
if (isInlineScript(manifest))
|
|
4727
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4728
|
+
const rel = scriptRelFor(manifest);
|
|
4729
|
+
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4730
|
+
if (!existsSync2(resolved)) {
|
|
4731
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4769
4732
|
}
|
|
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]);
|
|
4733
|
+
return { path: rel, content: readFileSync(resolved, "utf-8") };
|
|
4773
4734
|
}
|
|
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
|
-
});
|
|
4735
|
+
function customHookDir(name) {
|
|
4736
|
+
return join3(getCustomHooksDir(), shortManifestName(name));
|
|
4737
|
+
}
|
|
4738
|
+
function readCustomManifest(name) {
|
|
4739
|
+
const dir = customHookDir(name);
|
|
4740
|
+
const manifestPath = join3(dir, "manifest.json");
|
|
4741
|
+
if (!existsSync2(manifestPath))
|
|
4742
|
+
return;
|
|
4743
|
+
try {
|
|
4744
|
+
const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
|
|
4745
|
+
const script = resolveScript(manifest, dir);
|
|
4746
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4747
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4748
|
+
} catch (err) {
|
|
4749
|
+
if (err instanceof ScriptContainmentError)
|
|
4750
|
+
throw err;
|
|
4751
|
+
return;
|
|
4800
4752
|
}
|
|
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
4753
|
}
|
|
4815
|
-
function
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4754
|
+
function listCustomHooks() {
|
|
4755
|
+
const dir = getCustomHooksDir();
|
|
4756
|
+
if (!existsSync2(dir))
|
|
4757
|
+
return [];
|
|
4758
|
+
const results = [];
|
|
4759
|
+
for (const entry of readdirSafe(dir)) {
|
|
4760
|
+
const parsed = readCustomManifest(entry);
|
|
4761
|
+
if (parsed)
|
|
4762
|
+
results.push(parsed);
|
|
4820
4763
|
}
|
|
4821
|
-
return
|
|
4764
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4822
4765
|
}
|
|
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 [];
|
|
4766
|
+
function readdirSafe(dir) {
|
|
4767
|
+
try {
|
|
4768
|
+
return readdirSync(dir);
|
|
4769
|
+
} catch {
|
|
4770
|
+
return [];
|
|
5136
4771
|
}
|
|
5137
4772
|
}
|
|
5138
4773
|
|
|
5139
4774
|
// src/lib/store.ts
|
|
5140
4775
|
import { createHash } from "crypto";
|
|
5141
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as
|
|
4776
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5142
4777
|
import { readFile } from "fs/promises";
|
|
5143
4778
|
|
|
5144
4779
|
// src/db/index.ts
|
|
5145
4780
|
import { Database } from "bun:sqlite";
|
|
5146
4781
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
5147
|
-
import { join as
|
|
4782
|
+
import { join as join5 } from "path";
|
|
5148
4783
|
|
|
5149
4784
|
// src/db/schema.ts
|
|
5150
4785
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
@@ -5316,9 +4951,9 @@ function runMigrations(db) {
|
|
|
5316
4951
|
}
|
|
5317
4952
|
|
|
5318
4953
|
// src/db/legacy-import.ts
|
|
5319
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as
|
|
5320
|
-
import { join as
|
|
5321
|
-
import { homedir as
|
|
4954
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
4955
|
+
import { join as join4 } from "path";
|
|
4956
|
+
import { homedir as homedir2 } from "os";
|
|
5322
4957
|
var META_KEY = "legacy_import_done";
|
|
5323
4958
|
function ensureMetaTable(db) {
|
|
5324
4959
|
db.exec(`
|
|
@@ -5342,7 +4977,7 @@ function nanoid() {
|
|
|
5342
4977
|
function importJsonlFile(db, filePath) {
|
|
5343
4978
|
let count = 0;
|
|
5344
4979
|
try {
|
|
5345
|
-
const lines =
|
|
4980
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5346
4981
|
`).filter(Boolean);
|
|
5347
4982
|
for (const line of lines) {
|
|
5348
4983
|
try {
|
|
@@ -5368,7 +5003,7 @@ function importJsonlFile(db, filePath) {
|
|
|
5368
5003
|
function importErrorsLog(db, filePath) {
|
|
5369
5004
|
let count = 0;
|
|
5370
5005
|
try {
|
|
5371
|
-
const lines =
|
|
5006
|
+
const lines = readFileSync2(filePath, "utf-8").split(`
|
|
5372
5007
|
`).filter(Boolean);
|
|
5373
5008
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
5374
5009
|
for (const line of lines) {
|
|
@@ -5393,25 +5028,25 @@ function importErrorsLog(db, filePath) {
|
|
|
5393
5028
|
} catch {}
|
|
5394
5029
|
return count;
|
|
5395
5030
|
}
|
|
5396
|
-
function runLegacyImport(db,
|
|
5031
|
+
function runLegacyImport(db, homeDir = homedir2()) {
|
|
5397
5032
|
try {
|
|
5398
5033
|
if (isAlreadyDone(db))
|
|
5399
5034
|
return;
|
|
5400
5035
|
let total = 0;
|
|
5401
|
-
const claudeProjectsDir =
|
|
5036
|
+
const claudeProjectsDir = join4(homeDir, ".claude", "projects");
|
|
5402
5037
|
if (existsSync3(claudeProjectsDir)) {
|
|
5403
5038
|
try {
|
|
5404
5039
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
5405
5040
|
for (const dir of projectDirs) {
|
|
5406
|
-
const projectDir =
|
|
5041
|
+
const projectDir = join4(claudeProjectsDir, dir);
|
|
5407
5042
|
try {
|
|
5408
5043
|
const files = readdirSync2(projectDir);
|
|
5409
5044
|
for (const file of files) {
|
|
5410
5045
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
5411
|
-
total += importJsonlFile(db,
|
|
5046
|
+
total += importJsonlFile(db, join4(projectDir, file));
|
|
5412
5047
|
}
|
|
5413
5048
|
if (file === "errors.log") {
|
|
5414
|
-
total += importErrorsLog(db,
|
|
5049
|
+
total += importErrorsLog(db, join4(projectDir, file));
|
|
5415
5050
|
}
|
|
5416
5051
|
}
|
|
5417
5052
|
} catch {}
|
|
@@ -5447,7 +5082,7 @@ function runRetention(db, days) {
|
|
|
5447
5082
|
var instance = null;
|
|
5448
5083
|
function resolveDataDir() {
|
|
5449
5084
|
const effective = getEffectiveDataRoot();
|
|
5450
|
-
const oldDir =
|
|
5085
|
+
const oldDir = join5(getHomeDir(), ".hooks");
|
|
5451
5086
|
if (!existsSync4(effective) && existsSync4(oldDir)) {
|
|
5452
5087
|
mkdirSync2(effective, { recursive: true });
|
|
5453
5088
|
cpSync(oldDir, effective, { recursive: true });
|
|
@@ -5459,7 +5094,7 @@ function getDbPath() {
|
|
|
5459
5094
|
if (explicitDb)
|
|
5460
5095
|
return explicitDb;
|
|
5461
5096
|
const dataDir2 = resolveDataDir();
|
|
5462
|
-
return
|
|
5097
|
+
return join5(dataDir2, "hooks.db");
|
|
5463
5098
|
}
|
|
5464
5099
|
function ensureDir(dbPath) {
|
|
5465
5100
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
@@ -5558,7 +5193,7 @@ function readLock() {
|
|
|
5558
5193
|
return { hooks: {} };
|
|
5559
5194
|
let raw;
|
|
5560
5195
|
try {
|
|
5561
|
-
raw =
|
|
5196
|
+
raw = readFileSync3(path, "utf-8");
|
|
5562
5197
|
} catch {
|
|
5563
5198
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5564
5199
|
}
|
|
@@ -5590,7 +5225,7 @@ function setPinnedHook(name, entry) {
|
|
|
5590
5225
|
return writeLock(lock);
|
|
5591
5226
|
}
|
|
5592
5227
|
function retrustHook(name, scriptPath, version, source) {
|
|
5593
|
-
const actual = sha256Of(
|
|
5228
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5594
5229
|
const db = getDb();
|
|
5595
5230
|
upsertHookRecord(db, {
|
|
5596
5231
|
name,
|
|
@@ -5604,14 +5239,14 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
5604
5239
|
}
|
|
5605
5240
|
|
|
5606
5241
|
// src/lib/resolve.ts
|
|
5607
|
-
import { dirname as dirname2, join as
|
|
5242
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
5608
5243
|
import { existsSync as existsSync6 } from "fs";
|
|
5609
5244
|
import { fileURLToPath } from "url";
|
|
5610
5245
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
5611
|
-
const sourceLayout =
|
|
5612
|
-
if (existsSync6(
|
|
5246
|
+
const sourceLayout = join6(moduleDir, "..", "..", "hooks");
|
|
5247
|
+
if (existsSync6(join6(sourceLayout, "hook-gitguard")))
|
|
5613
5248
|
return sourceLayout;
|
|
5614
|
-
return
|
|
5249
|
+
return join6(moduleDir, "..", "hooks");
|
|
5615
5250
|
}
|
|
5616
5251
|
function resolveHookDir(name) {
|
|
5617
5252
|
const custom2 = readCustomManifest(name);
|
|
@@ -5622,10 +5257,10 @@ function resolveHookDir(name) {
|
|
|
5622
5257
|
return;
|
|
5623
5258
|
const short = shortManifestName(name);
|
|
5624
5259
|
const bundledBase = resolveBundledHooksDir();
|
|
5625
|
-
const direct =
|
|
5260
|
+
const direct = join6(bundledBase, short);
|
|
5626
5261
|
if (existsSync6(direct))
|
|
5627
5262
|
return direct;
|
|
5628
|
-
const prefixed =
|
|
5263
|
+
const prefixed = join6(bundledBase, `hook-${short}`);
|
|
5629
5264
|
return existsSync6(prefixed) ? prefixed : undefined;
|
|
5630
5265
|
}
|
|
5631
5266
|
function resolveScriptPath(name) {
|
|
@@ -5635,7 +5270,7 @@ function resolveScriptPath(name) {
|
|
|
5635
5270
|
const dir = resolveHookDir(name);
|
|
5636
5271
|
if (!dir)
|
|
5637
5272
|
return;
|
|
5638
|
-
const script =
|
|
5273
|
+
const script = join6(dir, "src", "hook.ts");
|
|
5639
5274
|
return existsSync6(script) ? script : undefined;
|
|
5640
5275
|
}
|
|
5641
5276
|
function resolveHookMeta(name) {
|
|
@@ -5689,9 +5324,554 @@ function resolveHook(name) {
|
|
|
5689
5324
|
|
|
5690
5325
|
// src/serve.ts
|
|
5691
5326
|
import { readFileSync as readFileSync5 } from "fs";
|
|
5692
|
-
import { dirname as dirname3, join as
|
|
5327
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
5693
5328
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5694
5329
|
|
|
5330
|
+
// ../contracts/dist/client/transport.js
|
|
5331
|
+
import { spawnSync } from "child_process";
|
|
5332
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync4 } from "fs";
|
|
5333
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5334
|
+
import { createRequire } from "module";
|
|
5335
|
+
import { hostname as osHostname } from "os";
|
|
5336
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
5337
|
+
function envToken(name) {
|
|
5338
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
5339
|
+
}
|
|
5340
|
+
function clientTransportEnvKeys(name) {
|
|
5341
|
+
const envSegment = envToken(name);
|
|
5342
|
+
return {
|
|
5343
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
5344
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5347
|
+
function credentialOverrideEnvKey(name) {
|
|
5348
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
5349
|
+
}
|
|
5350
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
5351
|
+
function credentialPointerEnvKey(name) {
|
|
5352
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5355
|
+
class CredentialResolutionError extends Error {
|
|
5356
|
+
appName;
|
|
5357
|
+
attempted;
|
|
5358
|
+
constructor(appName, message, attempted) {
|
|
5359
|
+
super(message);
|
|
5360
|
+
this.name = "CredentialResolutionError";
|
|
5361
|
+
this.appName = appName;
|
|
5362
|
+
this.attempted = attempted;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
class CredentialFileUnsafeError extends Error {
|
|
5367
|
+
path;
|
|
5368
|
+
constructor(path, reason) {
|
|
5369
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
5370
|
+
this.name = "CredentialFileUnsafeError";
|
|
5371
|
+
this.path = path;
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
5375
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
5376
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
5377
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
5378
|
+
var CONFIG_SUBDIR = "config";
|
|
5379
|
+
var CREDENTIALS_FILE = "credentials";
|
|
5380
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
5381
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
5382
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
5383
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
5384
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
5385
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
5386
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
5387
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
5388
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
5389
|
+
function homeDir(env) {
|
|
5390
|
+
const home = env.HOME?.trim();
|
|
5391
|
+
return home ? home : null;
|
|
5392
|
+
}
|
|
5393
|
+
function absoluteOverride(env, key) {
|
|
5394
|
+
const value = env[key]?.trim();
|
|
5395
|
+
return value && isAbsolute2(value) ? value : null;
|
|
5396
|
+
}
|
|
5397
|
+
function hasnaHomeDir(env) {
|
|
5398
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
5399
|
+
if (override)
|
|
5400
|
+
return override;
|
|
5401
|
+
const home = homeDir(env);
|
|
5402
|
+
return home ? join7(home, HASNA_HOME_DIR) : null;
|
|
5403
|
+
}
|
|
5404
|
+
function appConfigDir(name, env) {
|
|
5405
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
5406
|
+
if (configRoot)
|
|
5407
|
+
return join7(configRoot, name);
|
|
5408
|
+
const root = hasnaHomeDir(env);
|
|
5409
|
+
return root ? join7(root, name, CONFIG_SUBDIR) : null;
|
|
5410
|
+
}
|
|
5411
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
5412
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
5413
|
+
return [];
|
|
5414
|
+
const directory = appConfigDir(name, env);
|
|
5415
|
+
if (!directory)
|
|
5416
|
+
return [];
|
|
5417
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
5418
|
+
return [{ path: join7(directory, file), tier: "disk" }];
|
|
5419
|
+
}
|
|
5420
|
+
function credentialDiskSources(name, env) {
|
|
5421
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
5422
|
+
}
|
|
5423
|
+
function profileDiskSources(name, env, profile) {
|
|
5424
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
5425
|
+
}
|
|
5426
|
+
function parseEnvFile(text) {
|
|
5427
|
+
const values = new Map;
|
|
5428
|
+
const unusable = new Set;
|
|
5429
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5430
|
+
const line = rawLine.trim();
|
|
5431
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
5432
|
+
continue;
|
|
5433
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
5434
|
+
const equals = withoutExport.indexOf("=");
|
|
5435
|
+
if (equals <= 0)
|
|
5436
|
+
continue;
|
|
5437
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
5438
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5439
|
+
continue;
|
|
5440
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
5441
|
+
const quote = value[0];
|
|
5442
|
+
if (quote === '"' || quote === "'") {
|
|
5443
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
5444
|
+
unusable.add(key);
|
|
5445
|
+
continue;
|
|
5446
|
+
}
|
|
5447
|
+
value = value.slice(1, -1);
|
|
5448
|
+
}
|
|
5449
|
+
if (value.trim().length === 0) {
|
|
5450
|
+
unusable.add(key);
|
|
5451
|
+
continue;
|
|
5452
|
+
}
|
|
5453
|
+
if (values.has(key) && values.get(key) !== value)
|
|
5454
|
+
unusable.add(key);
|
|
5455
|
+
values.set(key, value);
|
|
5456
|
+
}
|
|
5457
|
+
return { values, unusable };
|
|
5458
|
+
}
|
|
5459
|
+
function configFileModeAllowed(mode) {
|
|
5460
|
+
const permissions = mode & 4095;
|
|
5461
|
+
return permissions === 256 || permissions === 384;
|
|
5462
|
+
}
|
|
5463
|
+
function configFileReadsCoherent(before, after) {
|
|
5464
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
5465
|
+
}
|
|
5466
|
+
function readAppConfigFile(path) {
|
|
5467
|
+
const unsafe = (reason) => {
|
|
5468
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
5469
|
+
};
|
|
5470
|
+
let fd = -1;
|
|
5471
|
+
try {
|
|
5472
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
5473
|
+
} catch (error) {
|
|
5474
|
+
const code = error.code;
|
|
5475
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
5476
|
+
return null;
|
|
5477
|
+
if (code === "ELOOP")
|
|
5478
|
+
unsafe("the path is a symlink");
|
|
5479
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
5480
|
+
}
|
|
5481
|
+
try {
|
|
5482
|
+
const before = fstatSync(fd);
|
|
5483
|
+
if (!before.isFile())
|
|
5484
|
+
unsafe("the path is not a regular file");
|
|
5485
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
5486
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
5487
|
+
}
|
|
5488
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
5489
|
+
if (uid !== undefined && before.uid !== uid)
|
|
5490
|
+
unsafe("the file is not owned by the current user");
|
|
5491
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
5492
|
+
unsafe("the file exceeds the size limit");
|
|
5493
|
+
const bytes = readFileSync4(fd);
|
|
5494
|
+
const after = fstatSync(fd);
|
|
5495
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
5496
|
+
unsafe("the file changed while being read");
|
|
5497
|
+
}
|
|
5498
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
5499
|
+
} finally {
|
|
5500
|
+
if (fd !== -1)
|
|
5501
|
+
closeSync(fd);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
5505
|
+
const parsed = readAppConfigFile(path);
|
|
5506
|
+
if (!parsed)
|
|
5507
|
+
return null;
|
|
5508
|
+
for (const key of apiKeyKeys) {
|
|
5509
|
+
if (parsed.unusable.has(key)) {
|
|
5510
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
5514
|
+
if (new Set(values).size > 1) {
|
|
5515
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
5516
|
+
}
|
|
5517
|
+
return values[0] ?? null;
|
|
5518
|
+
}
|
|
5519
|
+
function assertUsableCredential(appName, source, value) {
|
|
5520
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
5521
|
+
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]);
|
|
5522
|
+
}
|
|
5523
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
5524
|
+
return;
|
|
5525
|
+
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]);
|
|
5526
|
+
}
|
|
5527
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
5528
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
5529
|
+
function sealCredential(fields) {
|
|
5530
|
+
const { apiKey } = fields;
|
|
5531
|
+
const visible = {
|
|
5532
|
+
tier: fields.tier,
|
|
5533
|
+
source: fields.source,
|
|
5534
|
+
deliberate: fields.deliberate,
|
|
5535
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
5536
|
+
warning: fields.warning
|
|
5537
|
+
};
|
|
5538
|
+
const sealed = { ...visible };
|
|
5539
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
5540
|
+
value: apiKey,
|
|
5541
|
+
enumerable: false,
|
|
5542
|
+
writable: false,
|
|
5543
|
+
configurable: false
|
|
5544
|
+
});
|
|
5545
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
5546
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
5547
|
+
value: fields.pointerVaultKey,
|
|
5548
|
+
enumerable: false,
|
|
5549
|
+
writable: false,
|
|
5550
|
+
configurable: false
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
5554
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
5555
|
+
enumerable: false,
|
|
5556
|
+
writable: false,
|
|
5557
|
+
configurable: false
|
|
5558
|
+
});
|
|
5559
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
5560
|
+
value: true,
|
|
5561
|
+
enumerable: false,
|
|
5562
|
+
writable: false,
|
|
5563
|
+
configurable: false
|
|
5564
|
+
});
|
|
5565
|
+
return Object.freeze(sealed);
|
|
5566
|
+
}
|
|
5567
|
+
function firstEnvValue(env, keys) {
|
|
5568
|
+
for (const key of keys) {
|
|
5569
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
5570
|
+
continue;
|
|
5571
|
+
const value = env[key]?.trim();
|
|
5572
|
+
if (value)
|
|
5573
|
+
return { key, value };
|
|
5574
|
+
}
|
|
5575
|
+
return null;
|
|
5576
|
+
}
|
|
5577
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
5578
|
+
function isAmbientEnvironment(env) {
|
|
5579
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
5580
|
+
}
|
|
5581
|
+
function defaultKeychainRunner(argv) {
|
|
5582
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
5583
|
+
encoding: "utf8",
|
|
5584
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5585
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
5586
|
+
});
|
|
5587
|
+
return {
|
|
5588
|
+
status: result.status,
|
|
5589
|
+
stdout: result.stdout ?? "",
|
|
5590
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
function keychainTierEnabled(env, options) {
|
|
5594
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
5595
|
+
return false;
|
|
5596
|
+
if (options.enabled !== undefined)
|
|
5597
|
+
return options.enabled;
|
|
5598
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
5599
|
+
}
|
|
5600
|
+
function keychainAccount(env, options) {
|
|
5601
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
5602
|
+
if (station)
|
|
5603
|
+
return station;
|
|
5604
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
5605
|
+
if (host)
|
|
5606
|
+
return host;
|
|
5607
|
+
const user = env.USER?.trim();
|
|
5608
|
+
return user || null;
|
|
5609
|
+
}
|
|
5610
|
+
function keychainFailureHint(text) {
|
|
5611
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
5612
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
5613
|
+
return clean ? `: ${clean}` : "";
|
|
5614
|
+
}
|
|
5615
|
+
function readKeychainItem(name, env, kind, options) {
|
|
5616
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
5617
|
+
return null;
|
|
5618
|
+
const account = keychainAccount(env, options);
|
|
5619
|
+
if (!account)
|
|
5620
|
+
return null;
|
|
5621
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
5622
|
+
const source = `keychain:${service}@${account}`;
|
|
5623
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
5624
|
+
let result;
|
|
5625
|
+
try {
|
|
5626
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
5627
|
+
} catch (error) {
|
|
5628
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
5629
|
+
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]);
|
|
5630
|
+
}
|
|
5631
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
5632
|
+
return null;
|
|
5633
|
+
if (result.status !== 0) {
|
|
5634
|
+
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]);
|
|
5635
|
+
}
|
|
5636
|
+
const value = result.stdout.trim();
|
|
5637
|
+
if (!value) {
|
|
5638
|
+
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]);
|
|
5639
|
+
}
|
|
5640
|
+
return { value, source };
|
|
5641
|
+
}
|
|
5642
|
+
function snapshotClientEnvironment(name, env) {
|
|
5643
|
+
const keys = clientTransportEnvKeys(name);
|
|
5644
|
+
const ambient = isAmbientEnvironment(env);
|
|
5645
|
+
const snapshot = Object.create(null);
|
|
5646
|
+
for (const key of [
|
|
5647
|
+
...keys.apiUrlKeys,
|
|
5648
|
+
...keys.apiKeyKeys,
|
|
5649
|
+
credentialOverrideEnvKey(name),
|
|
5650
|
+
credentialPointerEnvKey(name),
|
|
5651
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
5652
|
+
"HOME",
|
|
5653
|
+
HASNA_HOME_ENV_KEY,
|
|
5654
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
5655
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
5656
|
+
"USER"
|
|
5657
|
+
]) {
|
|
5658
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
5659
|
+
if (!descriptor)
|
|
5660
|
+
continue;
|
|
5661
|
+
if (!("value" in descriptor)) {
|
|
5662
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
5663
|
+
}
|
|
5664
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
5665
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
5666
|
+
}
|
|
5667
|
+
snapshot[key] = descriptor.value;
|
|
5668
|
+
}
|
|
5669
|
+
if (ambient) {
|
|
5670
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
5671
|
+
value: true,
|
|
5672
|
+
enumerable: false,
|
|
5673
|
+
writable: false,
|
|
5674
|
+
configurable: false
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
return Object.freeze(snapshot);
|
|
5678
|
+
}
|
|
5679
|
+
function resolveCredential(name, env, options = {}) {
|
|
5680
|
+
env = snapshotClientEnvironment(name, env);
|
|
5681
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
5682
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
5683
|
+
if (options.apiKey !== undefined) {
|
|
5684
|
+
const explicitKey = options.apiKey.trim();
|
|
5685
|
+
if (!explicitKey) {
|
|
5686
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
5687
|
+
}
|
|
5688
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
5689
|
+
return sealCredential({
|
|
5690
|
+
apiKey: explicitKey,
|
|
5691
|
+
tier: "argument",
|
|
5692
|
+
source: "explicit apiKey argument",
|
|
5693
|
+
deliberate: true,
|
|
5694
|
+
diskCandidates: diskPaths,
|
|
5695
|
+
warning: null
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
5699
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
5700
|
+
if (overrideRaw !== undefined) {
|
|
5701
|
+
const override = overrideRaw.trim();
|
|
5702
|
+
if (!override) {
|
|
5703
|
+
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]);
|
|
5704
|
+
}
|
|
5705
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
5706
|
+
return sealCredential({
|
|
5707
|
+
apiKey: override,
|
|
5708
|
+
tier: "override",
|
|
5709
|
+
source: overrideKeyName,
|
|
5710
|
+
deliberate: true,
|
|
5711
|
+
diskCandidates: diskPaths,
|
|
5712
|
+
warning: null
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5715
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
5716
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
5717
|
+
if (pointerRaw !== undefined) {
|
|
5718
|
+
const pointer = pointerRaw.trim();
|
|
5719
|
+
if (!pointer) {
|
|
5720
|
+
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]);
|
|
5721
|
+
}
|
|
5722
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
5723
|
+
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]);
|
|
5724
|
+
}
|
|
5725
|
+
return sealCredential({
|
|
5726
|
+
apiKey: "",
|
|
5727
|
+
pointerVaultKey: pointer,
|
|
5728
|
+
tier: "pointer",
|
|
5729
|
+
source: pointerKeyName,
|
|
5730
|
+
deliberate: true,
|
|
5731
|
+
diskCandidates: diskPaths,
|
|
5732
|
+
warning: null
|
|
5733
|
+
});
|
|
5734
|
+
}
|
|
5735
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
5736
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
5737
|
+
}
|
|
5738
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
5739
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
5740
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
5741
|
+
}
|
|
5742
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
5743
|
+
if (profile) {
|
|
5744
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
5745
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
5746
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
5747
|
+
}
|
|
5748
|
+
const paths = profileDiskSources(name, env, profile);
|
|
5749
|
+
for (const path of paths) {
|
|
5750
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
5751
|
+
if (value) {
|
|
5752
|
+
assertUsableCredential(name, path, value);
|
|
5753
|
+
return sealCredential({
|
|
5754
|
+
apiKey: value,
|
|
5755
|
+
tier: "profile",
|
|
5756
|
+
source: path,
|
|
5757
|
+
deliberate: true,
|
|
5758
|
+
diskCandidates: paths,
|
|
5759
|
+
warning: null
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
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);
|
|
5764
|
+
}
|
|
5765
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
5766
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
5767
|
+
if (blankEnv) {
|
|
5768
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
5769
|
+
}
|
|
5770
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
5771
|
+
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));
|
|
5772
|
+
}
|
|
5773
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
5774
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
5775
|
+
if (keychainHit) {
|
|
5776
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
5777
|
+
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;
|
|
5778
|
+
return sealCredential({
|
|
5779
|
+
apiKey: keychainHit.value,
|
|
5780
|
+
tier: "keychain",
|
|
5781
|
+
source: keychainHit.source,
|
|
5782
|
+
deliberate: false,
|
|
5783
|
+
diskCandidates: diskPaths,
|
|
5784
|
+
warning
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
5788
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
5789
|
+
if (diskHits.length > 0) {
|
|
5790
|
+
const winner = diskHits[0];
|
|
5791
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
5792
|
+
const divergentSources = [
|
|
5793
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
5794
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
5795
|
+
];
|
|
5796
|
+
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;
|
|
5797
|
+
return sealCredential({
|
|
5798
|
+
apiKey: winner.value,
|
|
5799
|
+
tier: winner.src.tier,
|
|
5800
|
+
source: winner.src.path,
|
|
5801
|
+
deliberate: false,
|
|
5802
|
+
diskCandidates: diskPaths,
|
|
5803
|
+
warning
|
|
5804
|
+
});
|
|
5805
|
+
}
|
|
5806
|
+
if (envHit) {
|
|
5807
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
5808
|
+
return sealCredential({
|
|
5809
|
+
apiKey: envHit.value,
|
|
5810
|
+
tier: "env",
|
|
5811
|
+
source: envHit.key,
|
|
5812
|
+
deliberate: false,
|
|
5813
|
+
diskCandidates: diskPaths,
|
|
5814
|
+
warning: null
|
|
5815
|
+
});
|
|
5816
|
+
}
|
|
5817
|
+
return null;
|
|
5818
|
+
}
|
|
5819
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
5820
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
5821
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
5822
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
5823
|
+
"host",
|
|
5824
|
+
":authority",
|
|
5825
|
+
"forwarded",
|
|
5826
|
+
"x-forwarded-host",
|
|
5827
|
+
"x-original-host"
|
|
5828
|
+
]);
|
|
5829
|
+
|
|
5830
|
+
// src/lib/local-opt-in.ts
|
|
5831
|
+
function hooksAuthorityEnvKeys() {
|
|
5832
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
5833
|
+
return [
|
|
5834
|
+
...keys.apiUrlKeys,
|
|
5835
|
+
...keys.apiKeyKeys,
|
|
5836
|
+
credentialOverrideEnvKey("hooks"),
|
|
5837
|
+
credentialPointerEnvKey("hooks"),
|
|
5838
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
5839
|
+
];
|
|
5840
|
+
}
|
|
5841
|
+
function hooksResolverEnv(env) {
|
|
5842
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
5843
|
+
if (blanks.length === 0)
|
|
5844
|
+
return env;
|
|
5845
|
+
const next = { ...env };
|
|
5846
|
+
for (const key of blanks)
|
|
5847
|
+
delete next[key];
|
|
5848
|
+
return next;
|
|
5849
|
+
}
|
|
5850
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
5851
|
+
function isAmbientHooksEnv(env) {
|
|
5852
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
5853
|
+
return true;
|
|
5854
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
5855
|
+
}
|
|
5856
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
5857
|
+
const normalised = hooksResolverEnv(env);
|
|
5858
|
+
if (normalised === env)
|
|
5859
|
+
return { env: normalised, credentials };
|
|
5860
|
+
const keychain = { ...credentials.keychain };
|
|
5861
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
5862
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
5863
|
+
}
|
|
5864
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
// src/lib/transport.ts
|
|
5868
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
5869
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
5870
|
+
const tier1 = options.credentials?.apiKey;
|
|
5871
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
5872
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
5873
|
+
}
|
|
5874
|
+
|
|
5695
5875
|
// src/lib/secure-compare.ts
|
|
5696
5876
|
function secureEqual(a, b) {
|
|
5697
5877
|
const enc = new TextEncoder;
|
|
@@ -5850,7 +6030,7 @@ var openApiDocument = {
|
|
|
5850
6030
|
apiKey: {
|
|
5851
6031
|
type: "http",
|
|
5852
6032
|
scheme: "bearer",
|
|
5853
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
6033
|
+
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
6034
|
}
|
|
5855
6035
|
}
|
|
5856
6036
|
}
|
|
@@ -5862,7 +6042,7 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
5862
6042
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
5863
6043
|
function packageVersion() {
|
|
5864
6044
|
try {
|
|
5865
|
-
const pkgPath =
|
|
6045
|
+
const pkgPath = join8(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
5866
6046
|
return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
|
|
5867
6047
|
} catch {
|
|
5868
6048
|
return "0.0.0";
|
|
@@ -6020,12 +6200,11 @@ function resolveServeOptions(options) {
|
|
|
6020
6200
|
}
|
|
6021
6201
|
function startServeServer(options) {
|
|
6022
6202
|
const { port, host } = resolveServeOptions(options);
|
|
6023
|
-
const apiKey = resolveApiKey();
|
|
6024
6203
|
const server = Bun.serve({
|
|
6025
6204
|
hostname: host,
|
|
6026
6205
|
port,
|
|
6027
6206
|
async fetch(req) {
|
|
6028
|
-
return handleServeRequest(req,
|
|
6207
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
6029
6208
|
}
|
|
6030
6209
|
});
|
|
6031
6210
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|