@hasna/hooks 0.9.4 → 0.9.6
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 +40 -2
- package/bin/hooks-mcp.js +330 -149
- package/bin/index.js +974 -350
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +559 -165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +902 -290
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/lib/resolver-types.d.ts +1 -1
- package/dist/lib/sync.d.ts +3 -2
- package/dist/lib/transport.d.ts +5 -1
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +16 -17
- package/dist/sdk/index.js +447 -47
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +8 -5
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +104 -2
- package/scripts/verify-generated-artifacts.ts +237 -0
package/dist/sdk/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
//
|
|
2
|
+
// ../contracts/dist/client/transport.js
|
|
3
3
|
import { isIP } from "net";
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
|
-
import { closeSync, fstatSync, openSync, readFileSync } from "fs";
|
|
6
|
-
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
7
|
-
import { createRequire } from "module";
|
|
5
|
+
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
|
|
6
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
8
7
|
import { hostname as osHostname } from "os";
|
|
9
|
-
import { isAbsolute, join } from "path";
|
|
8
|
+
import { isAbsolute, join as join2 } from "path";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
|
+
import { closeSync, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "fs";
|
|
12
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
13
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
10
15
|
function envToken(name) {
|
|
11
16
|
return name.toUpperCase().replace(/-/g, "_");
|
|
12
17
|
}
|
|
@@ -24,6 +29,114 @@ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
|
24
29
|
function credentialPointerEnvKey(name) {
|
|
25
30
|
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
26
31
|
}
|
|
32
|
+
function e(e2, n, r) {
|
|
33
|
+
throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
|
|
34
|
+
}
|
|
35
|
+
function n(n2, i, o, f) {
|
|
36
|
+
let s, u, l = r(n2, o), c = function(e2) {
|
|
37
|
+
let n3 = new Set(["default", ...e2.conditions || []]);
|
|
38
|
+
return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
|
|
39
|
+
}(f || {}), a = i[l];
|
|
40
|
+
if (a === undefined) {
|
|
41
|
+
let e2, n3, r, t;
|
|
42
|
+
for (t in i)
|
|
43
|
+
n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
|
|
44
|
+
a = i[n3];
|
|
45
|
+
}
|
|
46
|
+
return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
|
|
47
|
+
let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
|
|
48
|
+
for (;t < i2; t++)
|
|
49
|
+
e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
|
|
50
|
+
}(s, u), s;
|
|
51
|
+
}
|
|
52
|
+
function r(e2, n2, r2) {
|
|
53
|
+
if (e2 === n2 || n2 === ".")
|
|
54
|
+
return ".";
|
|
55
|
+
let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
|
|
56
|
+
return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
|
|
57
|
+
}
|
|
58
|
+
function t(e2, n2, r2) {
|
|
59
|
+
if (e2) {
|
|
60
|
+
if (typeof e2 == "string")
|
|
61
|
+
return r2 && r2.add(e2), [e2];
|
|
62
|
+
let i, o;
|
|
63
|
+
if (Array.isArray(e2)) {
|
|
64
|
+
for (o = r2 || new Set, i = 0;i < e2.length; i++)
|
|
65
|
+
t(e2[i], n2, o);
|
|
66
|
+
if (!r2 && o.size)
|
|
67
|
+
return [...o];
|
|
68
|
+
} else
|
|
69
|
+
for (i in e2)
|
|
70
|
+
if (n2.has(i))
|
|
71
|
+
return t(e2[i], n2, r2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function o(e2, r2, t2) {
|
|
75
|
+
let i, o2 = e2.exports;
|
|
76
|
+
if (o2) {
|
|
77
|
+
if (typeof o2 == "string")
|
|
78
|
+
o2 = { ".": o2 };
|
|
79
|
+
else
|
|
80
|
+
for (i in o2) {
|
|
81
|
+
i[0] !== "." && (o2 = { ".": o2 });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return n(e2.name, o2, r2 || ".", t2);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var PACKAGE = "@hasna/secrets";
|
|
88
|
+
var MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
89
|
+
function entryPoint(directory) {
|
|
90
|
+
const root = realpathSync(directory);
|
|
91
|
+
const fd = openSync(join(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
92
|
+
let pkg;
|
|
93
|
+
try {
|
|
94
|
+
const stat = fstatSync(fd);
|
|
95
|
+
if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
|
|
96
|
+
throw new Error("Invalid Secrets package metadata");
|
|
97
|
+
pkg = JSON.parse(readFileSync(fd, "utf8"));
|
|
98
|
+
} finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
|
|
102
|
+
throw new Error("Invalid Secrets package identity");
|
|
103
|
+
}
|
|
104
|
+
const target = o(pkg, ".")?.[0];
|
|
105
|
+
if (typeof target !== "string" || !target.startsWith("./"))
|
|
106
|
+
throw new Error("Missing Secrets import export");
|
|
107
|
+
const parts = target.slice(2).split("/");
|
|
108
|
+
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
109
|
+
throw new Error("Invalid Secrets import export");
|
|
110
|
+
}
|
|
111
|
+
const file = realpathSync(join(root, ...parts));
|
|
112
|
+
const within = relative(root, file);
|
|
113
|
+
if (!within || within === ".." || within.startsWith(`..${sep}`) || !statSync(file).isFile()) {
|
|
114
|
+
throw new Error("Secrets import export escapes its package or is not a file");
|
|
115
|
+
}
|
|
116
|
+
return pathToFileURL(file).href;
|
|
117
|
+
}
|
|
118
|
+
function resolveInstalledSecrets(parentUrl) {
|
|
119
|
+
let directory = dirname(fileURLToPath(parentUrl));
|
|
120
|
+
for (;; ) {
|
|
121
|
+
if (basename(directory) !== "node_modules") {
|
|
122
|
+
const candidate = join(directory, "node_modules", PACKAGE);
|
|
123
|
+
let absent = false;
|
|
124
|
+
try {
|
|
125
|
+
lstatSync(candidate);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error.code !== "ENOENT")
|
|
128
|
+
throw error;
|
|
129
|
+
absent = true;
|
|
130
|
+
}
|
|
131
|
+
if (!absent)
|
|
132
|
+
return entryPoint(candidate);
|
|
133
|
+
}
|
|
134
|
+
const parent = dirname(directory);
|
|
135
|
+
if (parent === directory)
|
|
136
|
+
throw new Error("Secrets SDK is not installed for this consumer");
|
|
137
|
+
directory = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
27
140
|
|
|
28
141
|
class CredentialResolutionError extends Error {
|
|
29
142
|
appName;
|
|
@@ -72,14 +185,14 @@ function hasnaHomeDir(env) {
|
|
|
72
185
|
if (override)
|
|
73
186
|
return override;
|
|
74
187
|
const home = homeDir(env);
|
|
75
|
-
return home ?
|
|
188
|
+
return home ? join2(home, HASNA_HOME_DIR) : null;
|
|
76
189
|
}
|
|
77
190
|
function appConfigDir(name, env) {
|
|
78
191
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
79
192
|
if (configRoot)
|
|
80
|
-
return
|
|
193
|
+
return join2(configRoot, name);
|
|
81
194
|
const root = hasnaHomeDir(env);
|
|
82
|
-
return root ?
|
|
195
|
+
return root ? join2(root, name, CONFIG_SUBDIR) : null;
|
|
83
196
|
}
|
|
84
197
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
85
198
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -88,7 +201,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
88
201
|
if (!directory)
|
|
89
202
|
return [];
|
|
90
203
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
91
|
-
return [{ path:
|
|
204
|
+
return [{ path: join2(directory, file), tier: "disk" }];
|
|
92
205
|
}
|
|
93
206
|
function credentialDiskSources(name, env) {
|
|
94
207
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -142,7 +255,7 @@ function readAppConfigFile(path) {
|
|
|
142
255
|
};
|
|
143
256
|
let fd = -1;
|
|
144
257
|
try {
|
|
145
|
-
fd =
|
|
258
|
+
fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
146
259
|
} catch (error) {
|
|
147
260
|
const code = error.code;
|
|
148
261
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -152,7 +265,7 @@ function readAppConfigFile(path) {
|
|
|
152
265
|
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
153
266
|
}
|
|
154
267
|
try {
|
|
155
|
-
const before =
|
|
268
|
+
const before = fstatSync2(fd);
|
|
156
269
|
if (!before.isFile())
|
|
157
270
|
unsafe("the path is not a regular file");
|
|
158
271
|
if (!configFileModeAllowed(before.mode)) {
|
|
@@ -163,31 +276,39 @@ function readAppConfigFile(path) {
|
|
|
163
276
|
unsafe("the file is not owned by the current user");
|
|
164
277
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
165
278
|
unsafe("the file exceeds the size limit");
|
|
166
|
-
const bytes =
|
|
167
|
-
const after =
|
|
279
|
+
const bytes = readFileSync2(fd);
|
|
280
|
+
const after = fstatSync2(fd);
|
|
168
281
|
if (!configFileReadsCoherent(before, after)) {
|
|
169
282
|
unsafe("the file changed while being read");
|
|
170
283
|
}
|
|
171
284
|
return parseEnvFile(bytes.toString("utf8"));
|
|
172
285
|
} finally {
|
|
173
286
|
if (fd !== -1)
|
|
174
|
-
|
|
287
|
+
closeSync2(fd);
|
|
175
288
|
}
|
|
176
289
|
}
|
|
177
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
290
|
+
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
178
291
|
const parsed = readAppConfigFile(path);
|
|
179
292
|
if (!parsed)
|
|
180
293
|
return null;
|
|
181
|
-
for (const key of apiKeyKeys) {
|
|
294
|
+
for (const key of [...apiKeyKeys, pointerKey]) {
|
|
182
295
|
if (parsed.unusable.has(key)) {
|
|
183
296
|
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
184
297
|
}
|
|
185
298
|
}
|
|
186
299
|
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
300
|
+
const pointer = parsed.values.get(pointerKey)?.trim();
|
|
301
|
+
if (pointer !== undefined) {
|
|
302
|
+
if (!VAULT_POINTER_SHAPE.test(pointer))
|
|
303
|
+
throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
|
|
304
|
+
if (values.length)
|
|
305
|
+
throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
|
|
306
|
+
return { apiKey: "", pointerVaultKey: pointer };
|
|
307
|
+
}
|
|
187
308
|
if (new Set(values).size > 1) {
|
|
188
309
|
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
189
310
|
}
|
|
190
|
-
return values[0]
|
|
311
|
+
return values[0] === undefined ? null : { apiKey: values[0] };
|
|
191
312
|
}
|
|
192
313
|
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
193
314
|
function appConfigDiskValue(name, env, keys) {
|
|
@@ -262,6 +383,18 @@ function sealCredential(fields) {
|
|
|
262
383
|
});
|
|
263
384
|
return Object.freeze(sealed);
|
|
264
385
|
}
|
|
386
|
+
function explicitCredential(appName, apiKey) {
|
|
387
|
+
const source = "explicit apiKey option";
|
|
388
|
+
assertUsableCredential(appName, source, apiKey);
|
|
389
|
+
return sealCredential({
|
|
390
|
+
apiKey,
|
|
391
|
+
tier: "argument",
|
|
392
|
+
source,
|
|
393
|
+
deliberate: true,
|
|
394
|
+
diskCandidates: [],
|
|
395
|
+
warning: null
|
|
396
|
+
});
|
|
397
|
+
}
|
|
265
398
|
function firstEnvValue(env, keys) {
|
|
266
399
|
for (const key of keys) {
|
|
267
400
|
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
@@ -448,12 +581,13 @@ function resolveCredential(name, env, options = {}) {
|
|
|
448
581
|
}
|
|
449
582
|
const paths = profileDiskSources(name, env, profile);
|
|
450
583
|
for (const path of paths) {
|
|
451
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
584
|
+
const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
|
|
452
585
|
if (value) {
|
|
453
|
-
|
|
586
|
+
if (!value.pointerVaultKey)
|
|
587
|
+
assertUsableCredential(name, path, value.apiKey);
|
|
454
588
|
return sealCredential({
|
|
455
|
-
|
|
456
|
-
tier: "profile",
|
|
589
|
+
...value,
|
|
590
|
+
tier: value.pointerVaultKey ? "pointer" : "profile",
|
|
457
591
|
source: path,
|
|
458
592
|
deliberate: true,
|
|
459
593
|
diskCandidates: paths,
|
|
@@ -461,7 +595,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
461
595
|
});
|
|
462
596
|
}
|
|
463
597
|
}
|
|
464
|
-
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);
|
|
598
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} 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);
|
|
465
599
|
}
|
|
466
600
|
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
467
601
|
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
@@ -486,17 +620,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
486
620
|
});
|
|
487
621
|
}
|
|
488
622
|
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
489
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
623
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
|
|
490
624
|
if (diskHits.length > 0) {
|
|
491
625
|
const winner = diskHits[0];
|
|
492
|
-
|
|
626
|
+
if (winner.value.pointerVaultKey) {
|
|
627
|
+
return sealCredential({
|
|
628
|
+
...winner.value,
|
|
629
|
+
tier: "pointer",
|
|
630
|
+
source: winner.src.path,
|
|
631
|
+
deliberate: false,
|
|
632
|
+
diskCandidates: diskPaths,
|
|
633
|
+
warning: null
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
assertUsableCredential(name, winner.src.path, winner.value.apiKey);
|
|
493
637
|
const divergentSources = [
|
|
494
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
495
|
-
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
638
|
+
...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
|
|
639
|
+
...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
|
|
496
640
|
];
|
|
497
641
|
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;
|
|
498
642
|
return sealCredential({
|
|
499
|
-
apiKey: winner.value,
|
|
643
|
+
apiKey: winner.value.apiKey,
|
|
500
644
|
tier: winner.src.tier,
|
|
501
645
|
source: winner.src.path,
|
|
502
646
|
deliberate: false,
|
|
@@ -517,8 +661,54 @@ function resolveCredential(name, env, options = {}) {
|
|
|
517
661
|
}
|
|
518
662
|
return null;
|
|
519
663
|
}
|
|
520
|
-
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
521
664
|
var requireSecretsSdk = createRequire(import.meta.url);
|
|
665
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
666
|
+
const secretsEnv = snapshotClientEnvironment("secrets", env);
|
|
667
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
668
|
+
const pointerEnvKey = pointerResolution.source;
|
|
669
|
+
if (!vaultKey) {
|
|
670
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
671
|
+
}
|
|
672
|
+
if (name === "secrets") {
|
|
673
|
+
throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
|
|
674
|
+
}
|
|
675
|
+
let secretsSdk;
|
|
676
|
+
try {
|
|
677
|
+
const sdkUrl = resolveInstalledSecrets(import.meta.url);
|
|
678
|
+
secretsSdk = requireSecretsSdk(fileURLToPath2(sdkUrl));
|
|
679
|
+
} catch {
|
|
680
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
681
|
+
}
|
|
682
|
+
let client;
|
|
683
|
+
try {
|
|
684
|
+
const bootstrap = resolveCredential("secrets", secretsEnv);
|
|
685
|
+
if (!bootstrap || bootstrap.tier === "pointer") {
|
|
686
|
+
throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
|
|
687
|
+
}
|
|
688
|
+
client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
|
|
689
|
+
} catch {
|
|
690
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
691
|
+
}
|
|
692
|
+
let secret;
|
|
693
|
+
try {
|
|
694
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
695
|
+
} catch {
|
|
696
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
697
|
+
}
|
|
698
|
+
const value = secret.value;
|
|
699
|
+
if (!value) {
|
|
700
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
701
|
+
}
|
|
702
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
703
|
+
return sealCredential({
|
|
704
|
+
apiKey: value,
|
|
705
|
+
tier: "pointer",
|
|
706
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
707
|
+
deliberate: pointerResolution.deliberate,
|
|
708
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
709
|
+
warning: null
|
|
710
|
+
});
|
|
711
|
+
}
|
|
522
712
|
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
523
713
|
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
524
714
|
function defaultFleetGatewayBaseUrl(name) {
|
|
@@ -747,7 +937,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
747
937
|
// src/lib/app-home.ts
|
|
748
938
|
import { existsSync } from "fs";
|
|
749
939
|
import { homedir } from "os";
|
|
750
|
-
import { join as
|
|
940
|
+
import { join as join3, resolve } from "path";
|
|
751
941
|
import { homedir as pathsResolverHomedir } from "os";
|
|
752
942
|
import { join as pathsResolverJoin } from "path";
|
|
753
943
|
var PATHS_RESOLVER_KIND_ENV = {
|
|
@@ -816,13 +1006,13 @@ function getResolverDataRoot(env = process.env) {
|
|
|
816
1006
|
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
817
1007
|
}
|
|
818
1008
|
function getLegacyDataRoot(env = process.env) {
|
|
819
|
-
return
|
|
1009
|
+
return join3(getHomeDir(env), ".hasna", "hooks");
|
|
820
1010
|
}
|
|
821
1011
|
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
822
1012
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
823
1013
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
824
1014
|
return true;
|
|
825
|
-
return existsSync(
|
|
1015
|
+
return existsSync(join3(resolved, STORE_MARKER));
|
|
826
1016
|
}
|
|
827
1017
|
function getExplicitDataDir(env = process.env) {
|
|
828
1018
|
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
@@ -931,6 +1121,41 @@ function rethrowHooksAuthorityFailure(error) {
|
|
|
931
1121
|
}
|
|
932
1122
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
933
1123
|
}
|
|
1124
|
+
var authorityBindings = new WeakMap;
|
|
1125
|
+
function sameCredential(left, right) {
|
|
1126
|
+
return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
|
|
1127
|
+
}
|
|
1128
|
+
function changedBinding() {
|
|
1129
|
+
throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
|
|
1130
|
+
}
|
|
1131
|
+
async function resolveHooksRequestAuthority(authority) {
|
|
1132
|
+
if (authority.origin !== hooksRegistryOrigin(authority.v1BaseUrl))
|
|
1133
|
+
changedBinding();
|
|
1134
|
+
const binding = authorityBindings.get(authority);
|
|
1135
|
+
if (!binding)
|
|
1136
|
+
return { ...authority, apiKey: explicitCredential("hooks", authority.apiKey).apiKey };
|
|
1137
|
+
const snapshot = () => {
|
|
1138
|
+
const next = resolveHooksTransport(binding.env, binding.options);
|
|
1139
|
+
if (next.mode !== "remote" || !next.authority || next.authority.v1BaseUrl !== authority.v1BaseUrl)
|
|
1140
|
+
changedBinding();
|
|
1141
|
+
return { authority: next.authority, binding: authorityBindings.get(next.authority) };
|
|
1142
|
+
};
|
|
1143
|
+
const first = snapshot();
|
|
1144
|
+
const reviewed = snapshot();
|
|
1145
|
+
if (!sameCredential(first.binding.credential, reviewed.binding.credential))
|
|
1146
|
+
changedBinding();
|
|
1147
|
+
let credential = reviewed.binding.credential;
|
|
1148
|
+
try {
|
|
1149
|
+
if (credential.tier === "pointer")
|
|
1150
|
+
credential = await completePointerCredential("hooks", credential, binding.env);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
rethrowHooksAuthorityFailure(error);
|
|
1153
|
+
}
|
|
1154
|
+
const final = snapshot();
|
|
1155
|
+
if (!sameCredential(reviewed.binding.credential, final.binding.credential))
|
|
1156
|
+
changedBinding();
|
|
1157
|
+
return { ...reviewed.authority, apiKey: credential.apiKey };
|
|
1158
|
+
}
|
|
934
1159
|
function resolveHooksTransport(env = process.env, options = {}) {
|
|
935
1160
|
if (selectsHooksLocalStore(env)) {
|
|
936
1161
|
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
@@ -944,7 +1169,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
944
1169
|
try {
|
|
945
1170
|
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
946
1171
|
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
947
|
-
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
1172
|
+
credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
948
1173
|
});
|
|
949
1174
|
} catch (error) {
|
|
950
1175
|
rethrowHooksAuthorityFailure(error);
|
|
@@ -952,22 +1177,24 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
952
1177
|
if (!credential) {
|
|
953
1178
|
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
954
1179
|
}
|
|
1180
|
+
const authority = {
|
|
1181
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
1182
|
+
apiKey: credential.apiKey,
|
|
1183
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
1184
|
+
apiKeySource: credential.source,
|
|
1185
|
+
apiKeyTier: credential.tier,
|
|
1186
|
+
v1BaseUrl: resolution.baseUrl,
|
|
1187
|
+
warning: resolution.warning
|
|
1188
|
+
};
|
|
1189
|
+
authorityBindings.set(authority, { env, options, credential });
|
|
955
1190
|
return {
|
|
956
1191
|
mode: "remote",
|
|
957
1192
|
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
958
|
-
authority
|
|
959
|
-
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
960
|
-
apiKey: credential.apiKey,
|
|
961
|
-
apiUrlSource: resolution.apiUrlSource,
|
|
962
|
-
apiKeySource: credential.source,
|
|
963
|
-
apiKeyTier: credential.tier,
|
|
964
|
-
v1BaseUrl: resolution.baseUrl,
|
|
965
|
-
warning: resolution.warning
|
|
966
|
-
}
|
|
1193
|
+
authority
|
|
967
1194
|
};
|
|
968
1195
|
}
|
|
969
1196
|
|
|
970
|
-
// src/sdk/
|
|
1197
|
+
// src/sdk/authority.ts
|
|
971
1198
|
function resolveHooksSdkAuthority(env = process.env, options = {}) {
|
|
972
1199
|
if (selectsHooksLocalStore(env)) {
|
|
973
1200
|
throw new Error("REMOTE_COMMAND_UNSUPPORTED: @hasna/hooks/sdk is a hosted registry client and has no local mode; " + `${HOOKS_LOCAL_OPT_IN_HINT} selects the on-box SQLite store, which only the \`hooks\` CLI serves. ` + `Unset it and configure a registry credential (${HOOKS_CREDENTIAL_TIERS}).`);
|
|
@@ -978,20 +1205,187 @@ function resolveHooksSdkAuthority(env = process.env, options = {}) {
|
|
|
978
1205
|
}
|
|
979
1206
|
return transport.authority;
|
|
980
1207
|
}
|
|
1208
|
+
// src/sdk/generated.ts
|
|
1209
|
+
class ApiError extends Error {
|
|
1210
|
+
status;
|
|
1211
|
+
body;
|
|
1212
|
+
constructor(status, message, body) {
|
|
1213
|
+
super(message);
|
|
1214
|
+
this.status = status;
|
|
1215
|
+
this.body = body;
|
|
1216
|
+
this.name = "ApiError";
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
981
1219
|
|
|
1220
|
+
class HooksRegistryApiClient {
|
|
1221
|
+
baseUrl;
|
|
1222
|
+
apiKey;
|
|
1223
|
+
fetchImpl;
|
|
1224
|
+
baseHeaders;
|
|
1225
|
+
constructor(options) {
|
|
1226
|
+
if (!options.baseUrl)
|
|
1227
|
+
throw new Error("HooksRegistryApiClient requires a baseUrl.");
|
|
1228
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
1229
|
+
this.apiKey = options.apiKey;
|
|
1230
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1231
|
+
this.baseHeaders = options.headers ?? {};
|
|
1232
|
+
}
|
|
1233
|
+
async request(method, path, opts) {
|
|
1234
|
+
const url = new URL(this.baseUrl + path);
|
|
1235
|
+
if (opts.query) {
|
|
1236
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
1237
|
+
if (value === undefined || value === null)
|
|
1238
|
+
continue;
|
|
1239
|
+
if (Array.isArray(value)) {
|
|
1240
|
+
for (const item of value) {
|
|
1241
|
+
if (item !== undefined && item !== null)
|
|
1242
|
+
url.searchParams.append(key, String(item));
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
url.searchParams.set(key, String(value));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
1250
|
+
if (this.apiKey)
|
|
1251
|
+
headers["x-api-key"] = this.apiKey;
|
|
1252
|
+
let payload;
|
|
1253
|
+
if (opts.body !== undefined) {
|
|
1254
|
+
headers["Content-Type"] = "application/json";
|
|
1255
|
+
payload = JSON.stringify(opts.body);
|
|
1256
|
+
}
|
|
1257
|
+
const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
|
|
1258
|
+
const text = await response.text();
|
|
1259
|
+
const data = text ? (() => {
|
|
1260
|
+
try {
|
|
1261
|
+
return JSON.parse(text);
|
|
1262
|
+
} catch {
|
|
1263
|
+
return text;
|
|
1264
|
+
}
|
|
1265
|
+
})() : undefined;
|
|
1266
|
+
if (!response.ok) {
|
|
1267
|
+
throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
|
|
1268
|
+
}
|
|
1269
|
+
return data;
|
|
1270
|
+
}
|
|
1271
|
+
async getCatalog(init) {
|
|
1272
|
+
return this.request("GET", `/api/v1/catalog`, {
|
|
1273
|
+
body: undefined,
|
|
1274
|
+
query: undefined,
|
|
1275
|
+
init
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
async putHook(body, init) {
|
|
1279
|
+
return this.request("PUT", `/api/v1/hooks`, {
|
|
1280
|
+
body,
|
|
1281
|
+
query: undefined,
|
|
1282
|
+
init
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
async getHookArtifact(name, version, init) {
|
|
1286
|
+
return this.request("GET", `/api/v1/hooks/${encodeURIComponent(String(name))}/${encodeURIComponent(String(version))}`, {
|
|
1287
|
+
body: undefined,
|
|
1288
|
+
query: undefined,
|
|
1289
|
+
init
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async getLock(init) {
|
|
1293
|
+
return this.request("GET", `/api/v1/lock`, {
|
|
1294
|
+
body: undefined,
|
|
1295
|
+
query: undefined,
|
|
1296
|
+
init
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
async getHealth(init) {
|
|
1300
|
+
return this.request("GET", `/health`, {
|
|
1301
|
+
body: undefined,
|
|
1302
|
+
query: undefined,
|
|
1303
|
+
init
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
async getOpenApi(init) {
|
|
1307
|
+
return this.request("GET", `/openapi.json`, {
|
|
1308
|
+
body: undefined,
|
|
1309
|
+
query: undefined,
|
|
1310
|
+
init
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
async getReady(init) {
|
|
1314
|
+
return this.request("GET", `/ready`, {
|
|
1315
|
+
body: undefined,
|
|
1316
|
+
query: undefined,
|
|
1317
|
+
init
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
async getVersion(init) {
|
|
1321
|
+
return this.request("GET", `/version`, {
|
|
1322
|
+
body: undefined,
|
|
1323
|
+
query: undefined,
|
|
1324
|
+
init
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
// src/sdk/registry-client.ts
|
|
1329
|
+
function bindAuthority(client, authority) {
|
|
1330
|
+
for (const [key, value] of [
|
|
1331
|
+
["authorityOrigin", authority.origin],
|
|
1332
|
+
["apiKeySource", authority.apiKeySource],
|
|
1333
|
+
["apiUrlSource", authority.apiUrlSource]
|
|
1334
|
+
]) {
|
|
1335
|
+
Object.defineProperty(client, key, {
|
|
1336
|
+
value,
|
|
1337
|
+
enumerable: true,
|
|
1338
|
+
writable: false,
|
|
1339
|
+
configurable: false
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
return client;
|
|
1343
|
+
}
|
|
1344
|
+
function bindAuthorityFetch(authority, baseFetch = fetch) {
|
|
1345
|
+
const expectedOrigin = new URL(authority.origin).origin;
|
|
1346
|
+
return async (input, init) => {
|
|
1347
|
+
const resolved = await resolveHooksRequestAuthority(authority);
|
|
1348
|
+
const url = new URL(String(input));
|
|
1349
|
+
if (url.origin !== expectedOrigin) {
|
|
1350
|
+
throw new Error(`REMOTE_API_ORIGIN_MISMATCH: hooks registry request targeted ${url.origin}, ` + `not the resolved registry origin ${expectedOrigin}; no credential was sent.`);
|
|
1351
|
+
}
|
|
1352
|
+
const headers = new Headers(init?.headers);
|
|
1353
|
+
headers.set("x-api-key", resolved.apiKey);
|
|
1354
|
+
return baseFetch(url, {
|
|
1355
|
+
...init,
|
|
1356
|
+
headers,
|
|
1357
|
+
redirect: "manual"
|
|
1358
|
+
});
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function hooksRegistryApiClientFor(authority, options = {}) {
|
|
1362
|
+
const boundFetch = bindAuthorityFetch(authority, options.fetch ?? fetch);
|
|
1363
|
+
const client = new HooksRegistryApiClient({
|
|
1364
|
+
baseUrl: authority.origin,
|
|
1365
|
+
fetch: boundFetch
|
|
1366
|
+
});
|
|
1367
|
+
return bindAuthority(client, authority);
|
|
1368
|
+
}
|
|
1369
|
+
function createHooksRegistryApiClient(options = {}) {
|
|
1370
|
+
const authority = resolveHooksSdkAuthority(options.env ?? process.env, {
|
|
1371
|
+
credentials: options.credentials
|
|
1372
|
+
});
|
|
1373
|
+
return hooksRegistryApiClientFor(authority, { fetch: options.fetch });
|
|
1374
|
+
}
|
|
1375
|
+
// src/sdk/index.ts
|
|
982
1376
|
class HooksClient {
|
|
983
1377
|
origin;
|
|
984
1378
|
v1BaseUrl;
|
|
985
1379
|
apiKeySource;
|
|
986
1380
|
apiUrlSource;
|
|
987
|
-
#
|
|
1381
|
+
#authority;
|
|
988
1382
|
#fetch;
|
|
989
1383
|
constructor(authority, options = {}) {
|
|
990
1384
|
this.origin = authority.origin;
|
|
991
1385
|
this.v1BaseUrl = authority.v1BaseUrl;
|
|
992
1386
|
this.apiKeySource = authority.apiKeySource;
|
|
993
1387
|
this.apiUrlSource = authority.apiUrlSource;
|
|
994
|
-
this.#
|
|
1388
|
+
this.#authority = authority;
|
|
995
1389
|
this.#fetch = options.fetch ?? fetch;
|
|
996
1390
|
}
|
|
997
1391
|
urlFor(path) {
|
|
@@ -1023,13 +1417,15 @@ class HooksClient {
|
|
|
1023
1417
|
return { ...body, sha256: res.headers.get("x-hook-sha256") };
|
|
1024
1418
|
}
|
|
1025
1419
|
async request(path) {
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1420
|
+
const authority = await resolveHooksRequestAuthority(this.#authority);
|
|
1421
|
+
const url = `${authority.origin}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1422
|
+
const res = await this.#fetch(url, {
|
|
1423
|
+
headers: { accept: "application/json", "x-api-key": authority.apiKey },
|
|
1028
1424
|
redirect: "manual"
|
|
1029
1425
|
});
|
|
1030
1426
|
if (!res.ok) {
|
|
1031
1427
|
const code = res.status === 401 || res.status === 403 ? "REMOTE_API_CREDENTIAL_INVALID" : "REMOTE_API_HTTP_ERROR";
|
|
1032
|
-
throw new Error(`${code}: hooks registry ${
|
|
1428
|
+
throw new Error(`${code}: hooks registry ${url} responded ${res.status}`);
|
|
1033
1429
|
}
|
|
1034
1430
|
return res;
|
|
1035
1431
|
}
|
|
@@ -1045,6 +1441,10 @@ function createHooksClient(options = {}) {
|
|
|
1045
1441
|
export {
|
|
1046
1442
|
resolveHooksSdkAuthority,
|
|
1047
1443
|
hooksRegistryOrigin,
|
|
1444
|
+
hooksRegistryApiClientFor,
|
|
1445
|
+
createHooksRegistryApiClient,
|
|
1048
1446
|
createHooksClient,
|
|
1447
|
+
ApiError as HooksRegistryApiError,
|
|
1448
|
+
HooksRegistryApiClient,
|
|
1049
1449
|
HooksClient
|
|
1050
1450
|
};
|