nestweaver 9.0.5 → 9.1.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.
Files changed (3) hide show
  1. package/README.md +16 -2
  2. package/install.js +336 -50
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -8,11 +8,25 @@ binary matching this package's version from the project's GitHub Releases,
8
8
  verifies it against the SHA-256 published alongside the archive, and puts a
9
9
  `nestweaver` executable on your PATH.
10
10
 
11
+ **Lifecycle scripts must be enabled.** The binary is fetched in `postinstall`,
12
+ so `npm install --ignore-scripts` — and **pnpm 10+, which blocks lifecycle
13
+ scripts by default** — leave you with a wrapper and no binary. `nestweaver` then
14
+ exits 1 saying the binary was not found. With pnpm, allow it explicitly:
15
+
16
+ ```jsonc
17
+ // package.json
18
+ { "pnpm": { "onlyBuiltDependencies": ["nestweaver"] } }
19
+ ```
20
+
21
+ If your organisation disallows install scripts outright, install a release
22
+ archive from GitHub or build from source (`cargo install --locked --path .`)
23
+ instead.
24
+
11
25
  ## Platforms
12
26
 
13
27
  macOS and Linux, on x86_64 and arm64. On any other platform the install step
14
- exits without failing and prints the supported targets; use a release archive
15
- or a source build instead.
28
+ FAILS and prints the supported targets, rather than leaving you with a wrapper
29
+ that cannot run; use a release archive or a source build instead.
16
30
 
17
31
  Linux builds target **glibc 2.35**, which covers Ubuntu 22.04 LTS and newer and
18
32
  Debian 12. The archive includes the GCC 13 runtime LadybugDB needs and the npm
package/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
2
3
 
3
- const { execSync } = require("child_process");
4
+ const { execFileSync } = require("child_process");
4
5
  const fs = require("fs");
5
6
  const path = require("path");
6
7
 
@@ -11,72 +12,357 @@ const PLATFORM_MAP = {
11
12
  "linux-arm64": "aarch64-unknown-linux-gnu",
12
13
  };
13
14
 
14
- const key = `${process.platform}-${process.arch}`;
15
- const target = PLATFORM_MAP[key];
15
+ const repo = "Kehl-io/nestweaver";
16
+
17
+ // ── Decision logic ──────────────────────────────────────────────────────
18
+ // Pure, exported, and covered by install.test.js. Everything below the
19
+ // `main()` boundary does I/O and is not directly testable; keeping the
20
+ // judgements out here is what makes the installer assertable at all.
16
21
 
17
- if (!target) {
18
- console.warn(`Unsupported platform: ${key}`);
19
- console.warn("Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64");
20
- console.warn(
21
- "Unsupported platform. Install a verified GitHub Release archive for a supported target, or from a source checkout: cargo install --locked --path .",
22
+ /// Explicit boolean parsing for operator-facing env vars. `0`, `false`, `no`,
23
+ /// `off` and blank all mean OFF; anything else non-blank means ON.
24
+ function isTruthyEnv(value) {
25
+ const normalized = String(value || "").trim().toLowerCase();
26
+ if (normalized.length === 0) {
27
+ return false;
28
+ }
29
+ return !["0", "false", "no", "off"].includes(normalized);
30
+ }
31
+
32
+ /// Whether a GitHub Release may be trusted to anchor its own checksum.
33
+ ///
34
+ /// The checksum lives beside the archive in the same release, so an actor who
35
+ /// can replace release assets can replace both. Only an IMMUTABLE release makes
36
+ /// the checksum tamper-evidence rather than a self-signed claim.
37
+ ///
38
+ /// A wrong tag is always fatal. A non-immutable release is DISCLOSED, not
39
+ /// refused: immutable releases were enabled on this repository only recently,
40
+ /// so every version already on the npm registry reports `immutable: false`
41
+ /// (verified against the live API for v9.0.0, v9.0.4 and v9.0.5). Refusing them
42
+ /// would make `npm install nestweaver` fail for every published version --
43
+ /// a self-inflicted outage, not a security control. The checksum is still
44
+ /// fetched and still enforced; what is absent is tamper-evidence, and saying so
45
+ /// is the honest form. `NESTWEAVER_REQUIRE_IMMUTABLE_RELEASE=1` opts into the
46
+ /// strict contract for callers who want it.
47
+ function assessReleaseTrust(release, tag, env) {
48
+ const actualTag = release && release.tag_name;
49
+ if (actualTag !== tag) {
50
+ return {
51
+ failure: `GitHub Release tag ${JSON.stringify(actualTag)} does not match the requested ${tag}`,
52
+ warning: null,
53
+ };
54
+ }
55
+ if (release.immutable === true) {
56
+ return { failure: null, warning: null };
57
+ }
58
+ // Any non-blank value used to mean "strict", so an operator writing
59
+ // NESTWEAVER_REQUIRE_IMMUTABLE_RELEASE=0 to opt OUT was opted IN and every
60
+ // build failed.
61
+ const strict = isTruthyEnv(env && env.NESTWEAVER_REQUIRE_IMMUTABLE_RELEASE);
62
+ const detail =
63
+ `GitHub Release ${tag} is not immutable, so its published SHA-256 could have been ` +
64
+ `replaced together with its archive. The checksum is still verified, but it is not ` +
65
+ `tamper-evidence. Releases published before immutable releases were enabled on this ` +
66
+ `repository all report this.`;
67
+ if (strict) {
68
+ return {
69
+ failure: `${detail} NESTWEAVER_REQUIRE_IMMUTABLE_RELEASE is set, so this install is refused.`,
70
+ warning: null,
71
+ };
72
+ }
73
+ return {
74
+ failure: null,
75
+ warning: `${detail} Set NESTWEAVER_REQUIRE_IMMUTABLE_RELEASE=1 to refuse instead of warn.`,
76
+ };
77
+ }
78
+
79
+ /// Whether the binary that was just installed IS the version this package
80
+ /// claims. Executing `--version` only proves the file runs; comparing its
81
+ /// output is what proves identity, and the previous implementation piped that
82
+ /// output straight to the terminal and compared nothing.
83
+ function runtimeVersionFailure(versionOutput, expected) {
84
+ // First line only, and the version must be reported AS the version -- i.e.
85
+ // `<name> <version>`. Accepting the token anywhere in the output let prose
86
+ // such as "9.0.6 is not this build; actual 1.0.0" pass.
87
+ const firstLine = String(versionOutput || "").trim().split(/\r?\n/)[0] || "";
88
+ const tokens = firstLine.split(/\s+/).filter(Boolean);
89
+ if (tokens.length === 0) {
90
+ return `the installed binary printed no version output; expected ${expected}`;
91
+ }
92
+ // The version must sit where a version goes: `<name> <version>` (the shape
93
+ // `nestweaver --version` actually prints), or a bare version on its own.
94
+ // Exact token match, so 9.0.60 can never satisfy a request for 9.0.6.
95
+ const reported = tokens.length === 1 ? tokens[0] : tokens[1];
96
+ if (reported === expected) {
97
+ return null;
98
+ }
99
+ return `the installed binary reports ${JSON.stringify(
100
+ String(versionOutput).trim(),
101
+ )}, which does not contain the expected version ${expected}`;
102
+ }
103
+
104
+ /// Headers for the release-metadata call.
105
+ ///
106
+ /// Unauthenticated api.github.com allows 60 requests/hour/IP, so a shared CI
107
+ /// egress or a corporate NAT exhausts it and every install behind that address
108
+ /// fails. A token raises the ceiling to 5,000 and is already present in
109
+ /// virtually every CI environment.
110
+ ///
111
+ /// TWO SEPARATE PROPERTIES, and only one of them is about the URL. The URL is a
112
+ /// fixed api.github.com constant, so the credential cannot be sent anywhere
113
+ /// else. But the credential must also not be EXPOSED where it is sent from:
114
+ /// passing it as a curl argv element publishes it to every local user through
115
+ /// the process table (`/proc/<pid>/cmdline` is world-readable on Linux, which
116
+ /// is what CI runners are), so `fetchRelease` feeds these to curl on stdin via
117
+ /// `--config -` rather than as `-H` arguments.
118
+ function githubApiHeaders(env) {
119
+ const headers = [
120
+ "Accept: application/vnd.github+json",
121
+ "X-GitHub-Api-Version: 2026-03-10",
122
+ ];
123
+ const token = [env && env.GITHUB_TOKEN, env && env.GH_TOKEN]
124
+ .map((value) => String(value || "").trim())
125
+ .find((value) => value.length > 0);
126
+ if (token) {
127
+ headers.push(`Authorization: Bearer ${token}`);
128
+ }
129
+ return headers;
130
+ }
131
+
132
+ /// Turn a failed release-metadata call into something the caller can act on.
133
+ /// Failing closed is correct; failing closed without naming the remedy is what
134
+ /// makes a rate limit look like a broken package.
135
+ function describeReleaseApiFailure(failure, env) {
136
+ const status = failure && failure.status;
137
+ const body = String((failure && failure.body) || "").trim();
138
+ const authenticated = githubApiHeaders(env).some((header) =>
139
+ header.startsWith("Authorization"),
22
140
  );
23
- process.exit(0); // Don't break npm install
141
+ if (status === 401) {
142
+ return authenticated
143
+ ? `GitHub rejected the credential in GITHUB_TOKEN/GH_TOKEN (HTTP 401: ${body}). This endpoint needs no credential for a public repository -- unset that variable, or replace the expired or wrong-scope token.`
144
+ : `GitHub returned HTTP 401 for an unauthenticated request: ${body}`;
145
+ }
146
+ if ((status === 403 || status === 429) && /rate limit/i.test(body)) {
147
+ return authenticated
148
+ ? `the GitHub API rate limit was exhausted for the credential in use; wait for the limit to reset and retry: ${body}`
149
+ : `the GitHub API rate limit was exhausted for this IP address (unauthenticated callers get 60 requests/hour, and a shared CI or NAT egress exhausts that quickly). Export GITHUB_TOKEN or GH_TOKEN to raise it, or wait for the limit to reset: ${body}`;
150
+ }
151
+ if (status === 404) {
152
+ return `the GitHub Release was not found (HTTP 404); the tag may not be published yet: ${body}`;
153
+ }
154
+ return `could not read GitHub Release metadata${status ? ` (HTTP ${status})` : ""}: ${body}`;
24
155
  }
25
156
 
26
- const version = require("./package.json").version;
27
- const repo = "Kehl-io/nestweaver";
28
- // Release tags are `v<version>` (release-please config: include-component-in-tag
29
- // false) and assets are `nestweaver-v<version>-<target>.tar.gz` (see the
30
- // `build` job in .github/workflows/release-please.yml). Keep these in sync.
31
- const tag = `v${version}`;
32
- const archive = `nestweaver-${tag}-${target}.tar.gz`;
33
- const base = `https://github.com/${repo}/releases/download/${tag}`;
34
- const url = `${base}/${archive}`;
157
+ module.exports = {
158
+ assessReleaseTrust,
159
+ runtimeVersionFailure,
160
+ githubApiHeaders,
161
+ describeReleaseApiFailure,
162
+ };
163
+
164
+ // ── Installation ────────────────────────────────────────────────────────
165
+
166
+ /// One release-metadata request. Returns `{ status, body }`; never throws for
167
+ /// an HTTP error, so the caller can decide whether to retry.
168
+ ///
169
+ /// `--fail-with-body`, NOT `-f`. Plain `-f` suppresses the response body on an
170
+ /// HTTP error, which left `describeReleaseApiFailure` matching /rate limit/
171
+ /// against a body that could only ever be the three-digit status -- so the
172
+ /// rate-limit branch, the entire reason the token handling exists, was
173
+ /// unreachable. Verified: with `-f` the recovered body is the literal "404".
174
+ ///
175
+ /// Headers go over stdin via `--config -` so a bearer token never appears in
176
+ /// this process's argv.
177
+ function requestRelease(url, headers) {
178
+ // The URL stays a normal argument: it is not a secret, and keeping it in
179
+ // argv is what lets tooling (and the release gate's fake curl) see which
180
+ // request is being made. ONLY the headers go over stdin, because that is
181
+ // where the bearer token lives.
182
+ const args = [
183
+ "--fail-with-body",
184
+ "-sSL",
185
+ "--write-out",
186
+ "\n%{http_code}",
187
+ "--config",
188
+ "-",
189
+ url,
190
+ ];
191
+ const config = headers
192
+ .map((header) => `header = ${JSON.stringify(header)}`)
193
+ .join("\n");
194
+ let raw;
195
+ try {
196
+ raw = execFileSync("curl", args, {
197
+ input: config,
198
+ maxBuffer: 32 * 1024 * 1024,
199
+ }).toString();
200
+ } catch (error) {
201
+ const stdout = String((error && error.stdout) || "");
202
+ if (stdout.length === 0) {
203
+ // curl never ran, or died before writing: ENOENT / timeout / DNS.
204
+ // `error.message` is safe to surface here because these arms carry no
205
+ // argv -- and argv no longer carries the token in any case.
206
+ return { status: 0, body: String((error && error.message) || error) };
207
+ }
208
+ raw = stdout;
209
+ }
210
+ const lines = raw.split("\n");
211
+ const status = Number(lines.pop().trim()) || 0;
212
+ return { status, body: lines.join("\n").trim() };
213
+ }
35
214
 
36
- const binDir = path.join(__dirname, ".nestweaver-bin");
37
- fs.mkdirSync(binDir, { recursive: true });
38
- const archivePath = path.join(binDir, archive);
215
+ function fetchRelease(url, env) {
216
+ const headers = githubApiHeaders(env);
217
+ const authenticated = headers.some((header) => header.startsWith("Authorization"));
218
+ let result = requestRelease(url, headers);
39
219
 
40
- console.log(`Downloading NestWeaver v${version} for ${target}...`);
220
+ // This endpoint needs no credential for a public repository, so an ambient
221
+ // GITHUB_TOKEN/GH_TOKEN can only ever make it worse: `gh auth login` and
222
+ // composite workflows routinely export a stale or wrong-scope token, which
223
+ // turns a request that would have succeeded anonymously into a hard 401.
224
+ // Retry once without it rather than failing on a credential the caller never
225
+ // asked us to use. Rate-limited responses are NOT retried -- dropping the
226
+ // token there would make the limit stricter, not looser.
227
+ if (
228
+ authenticated &&
229
+ (result.status === 401 || (result.status === 403 && !/rate limit/i.test(result.body)))
230
+ ) {
231
+ const anonymous = headers.filter((header) => !header.startsWith("Authorization"));
232
+ const retry = requestRelease(url, anonymous);
233
+ if (retry.status === 200) {
234
+ result = retry;
235
+ }
236
+ }
237
+
238
+ if (result.status !== 200) {
239
+ throw new Error(describeReleaseApiFailure(result, env));
240
+ }
241
+ try {
242
+ return JSON.parse(result.body);
243
+ } catch (error) {
244
+ throw new Error(`GitHub Release metadata was not valid JSON: ${error.message}`);
245
+ }
246
+ }
247
+
248
+ function main() {
249
+ const key = `${process.platform}-${process.arch}`;
250
+ const target = PLATFORM_MAP[key];
251
+ const binDir = path.join(__dirname, ".nestweaver-bin");
252
+ const binaryPath = path.join(binDir, "nestweaver");
253
+ fs.mkdirSync(binDir, { recursive: true });
254
+ // Never let a failed upgrade leave the newly installed wrapper executing a
255
+ // binary from an older package version, or a previous run's disclosure
256
+ // describing a release this run did not install.
257
+ fs.rmSync(binaryPath, { force: true });
258
+ fs.rmSync(path.join(binDir, "RELEASE-NOT-IMMUTABLE.txt"), { force: true });
259
+
260
+ if (!target) {
261
+ console.error(`Unsupported platform: ${key}`);
262
+ console.error("Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64");
263
+ console.error(
264
+ "Unsupported platform. Install a verified GitHub Release archive for a supported target, or from a source checkout: cargo install --locked --path .",
265
+ );
266
+ process.exit(1);
267
+ }
41
268
 
42
- try {
43
- execSync(`curl -fsSL "${url}" -o "${archivePath}"`, { stdio: "inherit" });
269
+ const version = require("./package.json").version;
270
+ // Release tags are `v<version>` (release-please config: include-component-in-tag
271
+ // false) and assets are `nestweaver-v<version>-<target>.tar.gz` (see the
272
+ // `build` job in .github/workflows/release-please.yml). Keep these in sync.
273
+ const tag = `v${version}`;
274
+ const archive = `nestweaver-${tag}-${target}.tar.gz`;
275
+ const base = `https://github.com/${repo}/releases/download/${tag}`;
276
+ const url = `${base}/${archive}`;
277
+ const releaseApiUrl = `https://api.github.com/repos/${repo}/releases/tags/${tag}`;
278
+ const archivePath = path.join(binDir, archive);
279
+
280
+ console.log(`Downloading NestWeaver v${version} for ${target}...`);
44
281
 
45
- // Verify the SHA-256 the release ships alongside the archive — a mismatch
46
- // means a corrupted or tampered download, which must fail loudly.
47
282
  try {
48
- const expected = execSync(`curl -fsSL "${url}.sha256"`)
283
+ const trust = assessReleaseTrust(
284
+ fetchRelease(releaseApiUrl, process.env),
285
+ tag,
286
+ process.env,
287
+ );
288
+ if (trust.failure) {
289
+ throw new Error(trust.failure);
290
+ }
291
+ if (trust.warning) {
292
+ // npm >= 7 HIDES lifecycle-script output unless the script fails or
293
+ // --foreground-scripts is passed, so a console warning alone does not
294
+ // reach an ordinary `npm install` user -- which would make "disclosed"
295
+ // a claim this code does not deliver. Leave it on disk beside the binary
296
+ // as well, so the disclosure is greppable after the fact.
297
+ console.warn(`Warning: ${trust.warning}`);
298
+ try {
299
+ fs.writeFileSync(
300
+ path.join(binDir, "RELEASE-NOT-IMMUTABLE.txt"),
301
+ `${trust.warning}\n\nRelease: ${tag}\nArchive: ${url}\n`,
302
+ );
303
+ } catch {
304
+ // Best effort: a disclosure that cannot be written must not fail an
305
+ // install whose checksum verified.
306
+ }
307
+ }
308
+
309
+ execFileSync("curl", ["-fsSL", url, "-o", archivePath], { stdio: "inherit" });
310
+
311
+ // Verify the SHA-256 the release ships alongside the archive — a mismatch
312
+ // means a corrupted or tampered download. Missing, unreadable, or malformed
313
+ // checksum evidence is equally non-authoritative and must fail closed.
314
+ const expected = execFileSync("curl", ["-fsSL", `${url}.sha256`])
49
315
  .toString()
50
316
  .trim()
51
317
  .split(/\s+/)[0];
52
- const actual = execSync(`shasum -a 256 "${archivePath}"`)
318
+ const actual = execFileSync("shasum", ["-a", "256", archivePath])
53
319
  .toString()
54
320
  .trim()
55
321
  .split(/\s+/)[0];
56
- if (expected && actual && expected !== actual) {
57
- throw new Error(
58
- `checksum mismatch: expected ${expected}, got ${actual}`,
59
- );
322
+ if (!/^[0-9a-f]{64}$/i.test(expected) || !/^[0-9a-f]{64}$/i.test(actual)) {
323
+ throw new Error("release checksum evidence is missing or malformed");
324
+ }
325
+ if (expected.toLowerCase() !== actual.toLowerCase()) {
326
+ throw new Error(`checksum mismatch: expected ${expected}, got ${actual}`);
60
327
  }
61
- } catch (checksumErr) {
62
- if (String(checksumErr.message || "").includes("checksum mismatch")) {
63
- throw checksumErr; // integrity failure — do not install
328
+
329
+ execFileSync("tar", ["xz", "-C", binDir, "-f", archivePath], {
330
+ stdio: "inherit",
331
+ });
332
+ if (!fs.existsSync(binaryPath) || !fs.statSync(binaryPath).isFile()) {
333
+ throw new Error("verified archive did not contain the nestweaver executable");
334
+ }
335
+ fs.rmSync(archivePath, { force: true });
336
+ fs.chmodSync(binaryPath, 0o755);
337
+
338
+ // Capture, do not inherit: piping `--version` to the terminal proves the
339
+ // binary executes and compares nothing.
340
+ const reported = execFileSync(binaryPath, ["--version"]).toString();
341
+ const versionFailure = runtimeVersionFailure(reported, version);
342
+ if (versionFailure) {
343
+ throw new Error(versionFailure);
64
344
  }
65
- // Missing/unreadable .sha256 is not fatal; proceed with the download.
345
+ console.log(`NestWeaver installed successfully (${reported.trim()}).`);
346
+ console.log(
347
+ "Upgrading an existing pre-publication brain? Stop its daemon and run `nestweaver publication rebuild --config /path/to/instance.toml`; the incumbent remains recoverable until validated cutover.",
348
+ );
349
+ } catch (err) {
350
+ fs.rmSync(archivePath, { force: true });
351
+ // Extraction and the runtime identity check happen after checksum
352
+ // validation. A failure there must not leave a candidate binary that npm
353
+ // would expose.
354
+ fs.rmSync(binaryPath, { force: true });
355
+ console.error(`Failed to install NestWeaver binary: ${err.message}`);
356
+ console.error(` URL: ${url}`);
357
+ console.error(
358
+ "Download a verified GitHub Release archive, or build from a source checkout: cargo install --locked --path .",
359
+ );
360
+ process.exit(1);
66
361
  }
362
+ }
67
363
 
68
- execSync(`tar xz -C "${binDir}" -f "${archivePath}"`, { stdio: "inherit" });
69
- fs.rmSync(archivePath, { force: true });
70
- fs.chmodSync(path.join(binDir, "nestweaver"), 0o755);
71
- console.log("NestWeaver installed successfully.");
72
- console.log(
73
- "Upgrading an existing pre-publication brain? Stop its daemon and run `nestweaver publication rebuild --config /path/to/instance.toml`; the incumbent remains recoverable until validated cutover.",
74
- );
75
- } catch (err) {
76
- console.warn(`Failed to install NestWeaver binary: ${err.message}`);
77
- console.warn(` URL: ${url}`);
78
- console.warn(
79
- "Download a verified GitHub Release archive, or build from a source checkout: cargo install --locked --path .",
80
- );
81
- process.exit(0); // Don't break npm install on transient/network failures
364
+ // Only install when executed as the postinstall script; `require` must be free
365
+ // of side effects so the decision logic above can be tested at all.
366
+ if (require.main === module) {
367
+ main();
82
368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestweaver",
3
- "version": "9.0.5",
3
+ "version": "9.1.0",
4
4
  "description": "Code knowledge graph for AI agents — 42 MCP tools, 32 languages, graph visualization",
5
5
  "bin": {
6
6
  "nestweaver": "bin/nestweaver"
@@ -21,6 +21,9 @@
21
21
  "x64",
22
22
  "arm64"
23
23
  ],
24
+ "libc": [
25
+ "glibc"
26
+ ],
24
27
  "keywords": [
25
28
  "mcp",
26
29
  "code-graph",