nestweaver 9.1.0 → 9.3.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 +34 -25
- package/bin/nestweaver +170 -9
- package/package.json +10 -8
- package/install.js +0 -368
package/README.md
CHANGED
|
@@ -3,36 +3,45 @@
|
|
|
3
3
|
Code knowledge graph for AI agents — 42 MCP tools, 32 languages, graph
|
|
4
4
|
visualization.
|
|
5
5
|
|
|
6
|
-
This package is a thin wrapper
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`nestweaver`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
If your
|
|
22
|
-
|
|
23
|
-
instead
|
|
6
|
+
This package is a thin wrapper with **no install-time (`postinstall`) script**.
|
|
7
|
+
It ships one prebuilt-binary package per platform —
|
|
8
|
+
`nestweaver-darwin-arm64`, `nestweaver-darwin-x64`, `nestweaver-linux-arm64`,
|
|
9
|
+
`nestweaver-linux-x64` — as an `optionalDependencies` entry, the same pattern
|
|
10
|
+
esbuild, swc, and Rollup use. npm, pnpm, and Yarn each install only the one
|
|
11
|
+
matching your machine's `os`/`cpu` through their own ordinary dependency
|
|
12
|
+
resolution; the `nestweaver` executable on your PATH resolves and runs
|
|
13
|
+
whichever one actually landed, at invocation time.
|
|
14
|
+
|
|
15
|
+
**No lifecycle scripts to allow.** There is nothing for
|
|
16
|
+
`npm install --ignore-scripts` to skip, and pnpm 10+'s default block on
|
|
17
|
+
lifecycle scripts (which used to leave a wrapper with no binary and no
|
|
18
|
+
`pnpm.onlyBuiltDependencies` clue why) no longer applies — there is no script
|
|
19
|
+
to block.
|
|
20
|
+
|
|
21
|
+
If your package manager skipped optional dependencies entirely
|
|
22
|
+
(`--omit=optional`, `--no-optional`, or a lockfile resolved without one),
|
|
23
|
+
`nestweaver` exits 1 and names the exact optional package to install instead
|
|
24
|
+
of silently doing nothing. If your organisation disallows this platform
|
|
25
|
+
package entirely, install a release archive from GitHub or build from source
|
|
26
|
+
(`cargo install --locked --path .`) instead.
|
|
24
27
|
|
|
25
28
|
## Platforms
|
|
26
29
|
|
|
27
|
-
macOS and Linux, on x86_64 and arm64.
|
|
28
|
-
FAILS
|
|
29
|
-
|
|
30
|
+
macOS and Linux, on x86_64 and arm64. Every other platform has no published
|
|
31
|
+
optional package, so `nestweaver` FAILS with the supported-targets list at
|
|
32
|
+
invocation time rather than leaving you with a wrapper that silently cannot
|
|
33
|
+
run; use a release archive or a source build instead. The two Linux platform
|
|
34
|
+
packages additionally declare `libc: ["glibc"]` (musl/Alpine is not
|
|
35
|
+
currently supported). Declaring `libc` is safe on a Linux-only package; it was
|
|
36
|
+
not safe on the earlier combined macOS+Linux wrapper, where it made npm reject
|
|
37
|
+
every macOS install with `EBADPLATFORM` because `libc` is not a concept on
|
|
38
|
+
macOS. The field itself was never the problem — the package it sat on was.
|
|
30
39
|
|
|
31
40
|
Linux builds target **glibc 2.35**, which covers Ubuntu 22.04 LTS and newer and
|
|
32
|
-
Debian 12. The
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
Debian 12. The platform package includes the GCC 13 runtime LadybugDB needs
|
|
42
|
+
beside the binary. Check your glibc with `ldd --version`; on anything older
|
|
43
|
+
the binary will not start and the error names a missing `GLIBC_` symbol.
|
|
44
|
+
macOS builds target **13.3**.
|
|
36
45
|
|
|
37
46
|
## Upgrading an existing graph
|
|
38
47
|
|
package/bin/nestweaver
CHANGED
|
@@ -1,11 +1,172 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
3
|
-
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
4
|
-
BINARY="$DIR/.nestweaver-bin/nestweaver"
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
echo "NestWeaver binary not found. Install a verified GitHub Release archive, or from a source checkout run: cargo install --locked --path ." >&2
|
|
8
|
-
exit 1
|
|
9
|
-
fi
|
|
4
|
+
const path = require("path");
|
|
10
5
|
|
|
11
|
-
|
|
6
|
+
// ── Decision logic ──────────────────────────────────────────────────────
|
|
7
|
+
// Pure, exported, and covered by nestweaver.test.js. Everything below the
|
|
8
|
+
// `main()` boundary does I/O (module resolution, process spawn) and is not
|
|
9
|
+
// directly testable; keeping the judgements out here is what makes the
|
|
10
|
+
// launcher assertable at all.
|
|
11
|
+
//
|
|
12
|
+
// nw-425 / nw-433: this package used to have a `postinstall` that downloaded
|
|
13
|
+
// the matching GitHub Release archive at install time. pnpm 10+ blocks
|
|
14
|
+
// lifecycle scripts by default, so `pnpm add nestweaver` silently produced a
|
|
15
|
+
// wrapper with no binary and no indication why -- the documented workaround
|
|
16
|
+
// (`"pnpm": {"onlyBuiltDependencies": ["nestweaver"]}`) required knowing the
|
|
17
|
+
// failure existed in the first place. There is now no lifecycle script at
|
|
18
|
+
// all. Each supported platform ships as its own optionalDependency (the
|
|
19
|
+
// esbuild/@rollup/swc pattern): npm/pnpm/yarn install ONLY the one whose
|
|
20
|
+
// package.json `os`/`cpu` match the current machine, entirely through their
|
|
21
|
+
// own normal dependency resolution -- nothing here runs at install time.
|
|
22
|
+
// This file is what runs at INVOCATION time: it resolves which platform
|
|
23
|
+
// package actually landed and execs its binary.
|
|
24
|
+
|
|
25
|
+
const PLATFORM_PACKAGES = {
|
|
26
|
+
"darwin-x64": "nestweaver-darwin-x64",
|
|
27
|
+
"darwin-arm64": "nestweaver-darwin-arm64",
|
|
28
|
+
"linux-x64": "nestweaver-linux-x64",
|
|
29
|
+
"linux-arm64": "nestweaver-linux-arm64",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function platformKey(platform, arch) {
|
|
33
|
+
return `${platform}-${arch}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Which optional platform package this machine should have installed, or
|
|
37
|
+
/// `null` if none is published for it.
|
|
38
|
+
function platformPackageName(platform, arch) {
|
|
39
|
+
return PLATFORM_PACKAGES[platformKey(platform, arch)] || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Whether the running system is musl (Alpine and similar), not glibc.
|
|
43
|
+
///
|
|
44
|
+
/// Ported from the retired install.js unchanged (nw-433): `glibcVersionRuntime`
|
|
45
|
+
/// is populated by Node's own `process.report` only when Node itself is linked
|
|
46
|
+
/// against glibc, so a musl-linked Node (Alpine's system Node, every
|
|
47
|
+
/// `node:*-alpine`/`node:*-musl` image) never has it. THREE states, not two:
|
|
48
|
+
/// `reportAvailable` is separate from `glibcVersionRuntime` so "the report
|
|
49
|
+
/// could not be produced at all" (sandboxed container, unusual build) and
|
|
50
|
+
/// "the report WAS produced and shows no glibc" (genuine musl) cannot
|
|
51
|
+
/// collapse into the same `undefined` -- that collapse would refuse a working
|
|
52
|
+
/// glibc-Linux box whenever the report merely couldn't be read. An
|
|
53
|
+
/// unavailable report fails OPEN (false, i.e. "not musl, proceed"); only a
|
|
54
|
+
/// report that was actually read and shows no glibc fails closed.
|
|
55
|
+
function isMuslLinux(platform, glibcVersionRuntime, reportAvailable) {
|
|
56
|
+
if (platform !== "linux" || !reportAvailable) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return !glibcVersionRuntime;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function unsupportedPlatformMessage(platform, arch) {
|
|
63
|
+
return [
|
|
64
|
+
`Unsupported platform: ${platform}-${arch}`,
|
|
65
|
+
"Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64",
|
|
66
|
+
"Install from a source checkout instead: cargo install --locked --path .",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function muslMessage(platform, arch) {
|
|
71
|
+
return [
|
|
72
|
+
`Unsupported platform: ${platform}-${arch} (musl libc)`,
|
|
73
|
+
"NestWeaver's published Linux packages are glibc-only (nestweaver-linux-x64, nestweaver-linux-arm64). musl/Alpine is not currently supported.",
|
|
74
|
+
"Build from a source checkout instead: cargo install --locked --path .",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Why `require.resolve` on the platform package's binary failed. This is
|
|
79
|
+
/// reached only when `platformPackageName` already matched a real target --
|
|
80
|
+
/// the package manager should have installed it -- so the likely causes are
|
|
81
|
+
/// operator-controlled (optional dependencies were explicitly skipped) rather
|
|
82
|
+
/// than "wrong platform", and the message says so instead of guessing.
|
|
83
|
+
function missingPlatformPackageMessage(pkgName, version) {
|
|
84
|
+
return [
|
|
85
|
+
`NestWeaver binary not found: optional dependency "${pkgName}@${version}" is not installed.`,
|
|
86
|
+
"This usually means your package manager skipped optional dependencies",
|
|
87
|
+
"(npm/yarn --omit=optional or --no-optional, or a lockfile resolved without it).",
|
|
88
|
+
`Install it explicitly: npm install ${pkgName}@${version}`,
|
|
89
|
+
"Or install from a source checkout instead: cargo install --locked --path .",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Resolve the platform package's binary via Node's own module resolution
|
|
94
|
+
/// (`resolveFn`, ordinarily `require.resolve`) rather than constructing a
|
|
95
|
+
/// path by hand. Module resolution is what makes this correct under every
|
|
96
|
+
/// package manager's node_modules layout -- including pnpm's strict,
|
|
97
|
+
/// symlinked store, which is exactly the layout a hand-built relative path
|
|
98
|
+
/// would get wrong and the reason nw-425 existed. Takes `resolveFn` as a
|
|
99
|
+
/// parameter so this is testable without a real installed dependency tree.
|
|
100
|
+
function resolveBinaryPath(pkgName, resolveFn) {
|
|
101
|
+
try {
|
|
102
|
+
return { binaryPath: resolveFn(`${pkgName}/bin/nestweaver`), error: null };
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return { binaryPath: null, error: err };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
PLATFORM_PACKAGES,
|
|
110
|
+
platformKey,
|
|
111
|
+
platformPackageName,
|
|
112
|
+
isMuslLinux,
|
|
113
|
+
unsupportedPlatformMessage,
|
|
114
|
+
muslMessage,
|
|
115
|
+
missingPlatformPackageMessage,
|
|
116
|
+
resolveBinaryPath,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ── Execution ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function main() {
|
|
122
|
+
const { spawnSync } = require("child_process");
|
|
123
|
+
const platform = process.platform;
|
|
124
|
+
const arch = process.arch;
|
|
125
|
+
|
|
126
|
+
if (platform === "linux") {
|
|
127
|
+
let glibcVersionRuntime;
|
|
128
|
+
let reportAvailable = false;
|
|
129
|
+
try {
|
|
130
|
+
if (process.report && typeof process.report.getReport === "function") {
|
|
131
|
+
glibcVersionRuntime = process.report.getReport().header.glibcVersionRuntime;
|
|
132
|
+
reportAvailable = true;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
reportAvailable = false;
|
|
136
|
+
}
|
|
137
|
+
if (isMuslLinux(platform, glibcVersionRuntime, reportAvailable)) {
|
|
138
|
+
console.error(muslMessage(platform, arch));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pkgName = platformPackageName(platform, arch);
|
|
144
|
+
if (!pkgName) {
|
|
145
|
+
console.error(unsupportedPlatformMessage(platform, arch));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const version = require(path.join(__dirname, "..", "package.json")).version;
|
|
150
|
+
const { binaryPath, error } = resolveBinaryPath(pkgName, (specifier) =>
|
|
151
|
+
require.resolve(specifier),
|
|
152
|
+
);
|
|
153
|
+
if (error) {
|
|
154
|
+
console.error(missingPlatformPackageMessage(pkgName, version));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
159
|
+
if (result.error) {
|
|
160
|
+
console.error(
|
|
161
|
+
`Failed to execute NestWeaver binary at ${binaryPath}: ${result.error.message}`,
|
|
162
|
+
);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Only run when executed as the npm bin entry point; `require` must be free
|
|
169
|
+
// of side effects so the decision logic above can be tested at all.
|
|
170
|
+
if (require.main === module) {
|
|
171
|
+
main();
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestweaver",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.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"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
-
"install.js",
|
|
11
10
|
"README.md"
|
|
12
11
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"postinstall": "node install.js"
|
|
15
|
-
},
|
|
16
12
|
"os": [
|
|
17
13
|
"darwin",
|
|
18
14
|
"linux"
|
|
@@ -21,9 +17,15 @@
|
|
|
21
17
|
"x64",
|
|
22
18
|
"arm64"
|
|
23
19
|
],
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"optionalDependencies": {
|
|
24
|
+
"nestweaver-darwin-arm64": "9.3.0",
|
|
25
|
+
"nestweaver-darwin-x64": "9.3.0",
|
|
26
|
+
"nestweaver-linux-arm64": "9.3.0",
|
|
27
|
+
"nestweaver-linux-x64": "9.3.0"
|
|
28
|
+
},
|
|
27
29
|
"keywords": [
|
|
28
30
|
"mcp",
|
|
29
31
|
"code-graph",
|
package/install.js
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const { execFileSync } = require("child_process");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
|
|
8
|
-
const PLATFORM_MAP = {
|
|
9
|
-
"darwin-x64": "x86_64-apple-darwin",
|
|
10
|
-
"darwin-arm64": "aarch64-apple-darwin",
|
|
11
|
-
"linux-x64": "x86_64-unknown-linux-gnu",
|
|
12
|
-
"linux-arm64": "aarch64-unknown-linux-gnu",
|
|
13
|
-
};
|
|
14
|
-
|
|
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.
|
|
21
|
-
|
|
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"),
|
|
140
|
-
);
|
|
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}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
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
|
-
}
|
|
214
|
-
|
|
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);
|
|
219
|
-
|
|
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
|
-
}
|
|
268
|
-
|
|
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}...`);
|
|
281
|
-
|
|
282
|
-
try {
|
|
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`])
|
|
315
|
-
.toString()
|
|
316
|
-
.trim()
|
|
317
|
-
.split(/\s+/)[0];
|
|
318
|
-
const actual = execFileSync("shasum", ["-a", "256", archivePath])
|
|
319
|
-
.toString()
|
|
320
|
-
.trim()
|
|
321
|
-
.split(/\s+/)[0];
|
|
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}`);
|
|
327
|
-
}
|
|
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);
|
|
344
|
-
}
|
|
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);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
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();
|
|
368
|
-
}
|