@hasna/hooks 0.9.3 → 0.9.5
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 +13 -2
- package/bin/hooks-mcp.js +103 -53
- package/bin/index.js +407 -128
- package/bin/serve.js +230 -38
- package/dist/index.d.ts +1 -1
- package/dist/index.js +344 -65
- 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/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +275 -46
- package/package.json +3 -3
- package/scripts/validate-package.ts +70 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
|
|
6
6
|
type PackageJson = {
|
|
7
7
|
name: string;
|
|
8
8
|
types?: string;
|
|
9
9
|
exports?: Record<string, unknown>;
|
|
10
|
+
version?: string;
|
|
11
|
+
dependencies?: Record<string, string>;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
type PackEntry = {
|
|
@@ -142,7 +144,72 @@ async function main(): Promise<void> {
|
|
|
142
144
|
await validateConsumerImports(pkg);
|
|
143
145
|
await validateExtractedSmoke(pkg);
|
|
144
146
|
|
|
145
|
-
console.log("Package validation passed: packed declarations are present, Bundler/NodeNext TypeScript consumer imports resolve, and the extracted tarball smoke-tests CLI help, serve /health, MCP startup, SDK import and one bundled-hook run (P3-17).");
|
|
147
|
+
console.log("Package validation passed: packed declarations are present, Bundler/NodeNext TypeScript consumer imports resolve, and the extracted tarball smoke-tests CLI help, serve /health, MCP startup, SDK import, credential-file vault references and one bundled-hook run (P3-17).");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function validatePackedCredentialReferences(pkg: PackageJson, packageDir: string, smokeDir: string): Promise<void> {
|
|
151
|
+
// Link the consumer's declared installed dependency, never the workspace's
|
|
152
|
+
// unrelated Secrets source. Bundling changes where the shared resolver finds it.
|
|
153
|
+
let parent = root;
|
|
154
|
+
let secretsDir: string | undefined;
|
|
155
|
+
for (;;) {
|
|
156
|
+
const candidate = join(parent, "node_modules", "@hasna", "secrets");
|
|
157
|
+
if (existsSync(candidate)) { secretsDir = await realpath(candidate); break; }
|
|
158
|
+
if (dirname(parent) === parent) throw new Error("Hooks Secrets dependency is not installed");
|
|
159
|
+
parent = dirname(parent);
|
|
160
|
+
}
|
|
161
|
+
const secretsPkg = await readPackageJson(join(secretsDir, "package.json"));
|
|
162
|
+
if (secretsPkg.name !== "@hasna/secrets" || secretsPkg.version !== pkg.dependencies?.["@hasna/secrets"]) {
|
|
163
|
+
throw new Error("Packed reference validation requires the exact declared Secrets dependency");
|
|
164
|
+
}
|
|
165
|
+
const peerDir = join(packageDir, "node_modules", "@hasna");
|
|
166
|
+
await mkdir(peerDir, { recursive: true });
|
|
167
|
+
await symlink(secretsDir, join(peerDir, "secrets"), "dir");
|
|
168
|
+
const fixture = join(smokeDir, "credential-reference-smoke.ts");
|
|
169
|
+
await writeFile(fixture, `
|
|
170
|
+
import { strict as assert } from "node:assert";
|
|
171
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
172
|
+
import { join } from "node:path";
|
|
173
|
+
import { createHooksClient } from "@hasna/hooks/sdk";
|
|
174
|
+
const home = join(import.meta.dir, "reference-home");
|
|
175
|
+
for (const [name, body] of [
|
|
176
|
+
["hooks", "HASNA_HOOKS_API_URL=https://hooks.example.test\\nHASNA_HOOKS_API_KEY_REF=fixture/hooks/key\\n"],
|
|
177
|
+
["secrets", "HASNA_SECRETS_API_URL=https://secrets.example.test\\nHASNA_SECRETS_API_KEY=fixture-bootstrap-key\\n"],
|
|
178
|
+
]) {
|
|
179
|
+
const folder = join(home, ".hasna", name, "config");
|
|
180
|
+
mkdirSync(folder, { recursive: true });
|
|
181
|
+
writeFileSync(join(folder, "credentials"), body, { mode: 0o600 });
|
|
182
|
+
}
|
|
183
|
+
let value = "fixture-first-key", vaultStatus = 200, reads = 0;
|
|
184
|
+
const keys = [];
|
|
185
|
+
globalThis.fetch = async (input, init) => {
|
|
186
|
+
const url = new URL(String(input)), headers = new Headers(init?.headers);
|
|
187
|
+
if (url.origin === "https://secrets.example.test") {
|
|
188
|
+
assert.equal(url.pathname, "/v1/secrets/get");
|
|
189
|
+
assert.equal(url.searchParams.get("key"), "fixture/hooks/key");
|
|
190
|
+
assert.equal(headers.get("x-api-key"), "fixture-bootstrap-key");
|
|
191
|
+
reads++;
|
|
192
|
+
return Response.json(vaultStatus === 200 ? { value } : { error: "fixture denial" }, { status: vaultStatus });
|
|
193
|
+
}
|
|
194
|
+
assert.equal(url.origin, "https://hooks.example.test");
|
|
195
|
+
assert.equal(init.redirect, "manual");
|
|
196
|
+
keys.push(headers.get("x-api-key"));
|
|
197
|
+
return Response.json({ hooks: [] });
|
|
198
|
+
};
|
|
199
|
+
const client = createHooksClient({ env: { HOME: home }, credentials: { keychain: { enabled: false } } });
|
|
200
|
+
await client.catalog();
|
|
201
|
+
value = "fixture-rotated-key";
|
|
202
|
+
await client.catalog();
|
|
203
|
+
vaultStatus = 401;
|
|
204
|
+
await assert.rejects(client.catalog(), /vault.*unavailable/);
|
|
205
|
+
assert.deepEqual(keys, ["fixture-first-key", "fixture-rotated-key"]);
|
|
206
|
+
assert.equal(reads, 3);
|
|
207
|
+
console.log("packed credential-file reference, rotation and terminal failure passed");
|
|
208
|
+
`);
|
|
209
|
+
const result = spawnSync("bun", ["run", fixture], {
|
|
210
|
+
cwd: smokeDir, env: { PATH: process.env.PATH }, encoding: "utf8", timeout: 20000,
|
|
211
|
+
});
|
|
212
|
+
if (result.status !== 0) throw new Error(`Packed credential reference failed: ${result.stderr}`);
|
|
146
213
|
}
|
|
147
214
|
|
|
148
215
|
/**
|
|
@@ -284,6 +351,7 @@ async function validateExtractedSmoke(pkg: PackageJson): Promise<void> {
|
|
|
284
351
|
const sdkOut = JSON.parse(sdk.stdout.trim()) as { count: number; backend: string; sdk: string; hasClient: boolean };
|
|
285
352
|
if (typeof sdkOut.count !== "number" || sdkOut.count <= 0) throw new Error(`packed SDK import returned count ${sdkOut.count}`);
|
|
286
353
|
if (sdkOut.sdk !== "fail-closed" || !sdkOut.hasClient) throw new Error(`packed ./sdk export did not fail closed with nothing configured: ${JSON.stringify(sdkOut)}`);
|
|
354
|
+
await validatePackedCredentialReferences(pkg, packageDir, smokeDir);
|
|
287
355
|
|
|
288
356
|
// 5. One bundled-hook run from the packed artifact (isolated data dir;
|
|
289
357
|
// first run self-trusts, then executes).
|