whitemagic-mcp 9.1.6 → 9.1.7

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/bin/cli.mjs +16 -64
  2. package/bin/lib.mjs +155 -0
  3. package/package.json +5 -2
package/bin/cli.mjs CHANGED
@@ -1,80 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * whitemagic-mcp — installs and runs the `wm` binary from the GitHub
4
- * releases, with checksum verification. `npx whitemagic-mcp serve` is
5
- * the MCP entrypoint. No dependencies; Node 18+ global fetch.
4
+ * releases, with checksum verification and a self-verifying cache.
5
+ * `npx whitemagic-mcp serve` is the MCP entrypoint. No dependencies;
6
+ * Node 18+ global fetch.
6
7
  *
7
8
  * Version contract: package version tracks the release tag (9.1.4 -> v9.1.4).
8
9
  * Override the tag with WHITEMAGIC_RELEASE (e.g. "v9").
10
+ *
11
+ * All install failures (network, checksum, cache, unsupported platform)
12
+ * terminate with a one-line human message — never a raw Node stack
13
+ * (2026-09-15 audit).
9
14
  */
10
- import { createHash } from "node:crypto";
11
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
- import { homedir, platform, arch, tmpdir } from "node:os";
13
- import { join } from "node:path";
15
+ import { readFileSync } from "node:fs";
16
+ import { platform } from "node:os";
14
17
  import { spawnSync } from "node:child_process";
18
+ import { ensureBinary } from "./lib.mjs";
15
19
 
16
20
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
17
- const TAG = process.env.WHITEMAGIC_RELEASE ?? `v${pkg.version}`;
18
- const REPO = "lbailey94/whitemagic";
19
- const BASE = `https://github.com/${REPO}/releases/download/${TAG}`;
20
-
21
- function assetFor() {
22
- const p = platform(), a = arch();
23
- if (p === "linux" && a === "x64") return "wm-linux-x86_64-musl";
24
- if (p === "darwin" && a === "arm64") return "wm-macos-aarch64";
25
- if (p === "darwin" && a === "x64") return "wm-macos-x86_64";
26
- if (p === "win32" && a === "x64") return "wm-windows-x86_64.exe";
27
- return null;
28
- }
29
-
30
- async function fetchTo(url, dest) {
31
- const res = await fetch(url, { redirect: "follow" });
32
- if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
33
- writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
34
- }
35
-
36
- async function ensureBinary() {
37
- const asset = assetFor();
38
- if (!asset) {
39
- console.error(`whitemagic-mcp: no release asset for ${platform()}/${arch()} yet.`);
40
- console.error(`Install from source instead: https://github.com/${REPO}#install`);
41
- process.exit(1);
42
- }
43
- const cached = join(
44
- process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"),
45
- "whitemagic",
46
- "bin",
47
- TAG,
48
- asset,
49
- );
50
- if (existsSync(cached)) return cached;
51
21
 
52
- console.error(`whitemagic-mcp: fetching ${TAG}/${asset} ...`);
53
- const binDir = join(
54
- process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"),
55
- "whitemagic",
56
- "bin",
57
- TAG,
58
- );
59
- mkdirSync(binDir, { recursive: true });
60
- const tmp = mkdtempSync(join(tmpdir(), "wm-dl-"));
61
- try {
62
- await fetchTo(`${BASE}/${asset}`, join(tmp, asset));
63
- await fetchTo(`${BASE}/${asset}.sha256`, join(tmp, `${asset}.sha256`));
64
- const expected = readFileSync(join(tmp, `${asset}.sha256`), "utf8").trim().split(/\s+/)[0];
65
- const actual = createHash("sha256").update(readFileSync(join(tmp, asset))).digest("hex");
66
- if (expected !== actual) {
67
- throw new Error(`checksum mismatch for ${asset}\n expected ${expected}\n actual ${actual}`);
68
- }
69
- writeFileSync(cached, readFileSync(join(tmp, asset)));
70
- chmodSync(cached, 0o755);
71
- return cached;
72
- } finally {
73
- rmSync(tmp, { recursive: true, force: true });
74
- }
22
+ let bin;
23
+ try {
24
+ bin = await ensureBinary({ pkgVersion: pkg.version });
25
+ } catch (err) {
26
+ console.error(`whitemagic-mcp: ${err instanceof Error ? err.message : String(err)}`);
27
+ process.exit(1);
75
28
  }
76
29
 
77
- const bin = await ensureBinary();
78
30
  const isWin = platform() === "win32";
79
31
  const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit", shell: isWin });
80
32
  process.exit(result.status ?? 1);
package/bin/lib.mjs ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * whitemagic-mcp launcher library — release-asset resolution, atomic
3
+ * download + checksum verification, and self-healing binary cache.
4
+ *
5
+ * Extracted from bin/cli.mjs so the install path is unit-testable
6
+ * (2026-09-15 audit: raw stacks on network/checksum failures; cached
7
+ * binaries were trusted without re-hashing; downloads were written
8
+ * in place instead of atomically).
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import {
12
+ chmodSync,
13
+ existsSync,
14
+ mkdirSync,
15
+ mkdtempSync,
16
+ readFileSync,
17
+ renameSync,
18
+ rmSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { homedir, platform, arch, tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+
24
+ export const REPO = "lbailey94/whitemagic";
25
+ const SHA256_RE = /^[0-9a-f]{64}$/i;
26
+
27
+ export function releaseTag(pkgVersion, env = process.env) {
28
+ return env.WHITEMAGIC_RELEASE ?? `v${pkgVersion}`;
29
+ }
30
+
31
+ export function assetFor(p = platform(), a = arch()) {
32
+ if (p === "linux" && a === "x64") return "wm-linux-x86_64-musl";
33
+ if (p === "darwin" && a === "arm64") return "wm-macos-aarch64";
34
+ if (p === "darwin" && a === "x64") return "wm-macos-x86_64";
35
+ if (p === "win32" && a === "x64") return "wm-windows-x86_64.exe";
36
+ return null;
37
+ }
38
+
39
+ export function defaultCacheRoot(env = process.env) {
40
+ return env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
41
+ }
42
+
43
+ export function cachedBinaryPath(asset, tag, cacheRoot = defaultCacheRoot()) {
44
+ return join(cacheRoot, "whitemagic", "bin", tag, asset);
45
+ }
46
+
47
+ function sidecarFor(cached) {
48
+ return `${cached}.sha256`;
49
+ }
50
+
51
+ export function digestFile(path) {
52
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
53
+ }
54
+
55
+ /**
56
+ * A cache entry counts only when the binary re-hashes to the digest the
57
+ * launcher recorded when it verified the download. A truncated or tampered
58
+ * cache thus FAILS verification and is re-acquired instead of being
59
+ * executed blindly.
60
+ */
61
+ export function cacheIsValid(cached) {
62
+ const sidecar = sidecarFor(cached);
63
+ if (!existsSync(cached) || !existsSync(sidecar)) return false;
64
+ let expected;
65
+ try {
66
+ expected = readFileSync(sidecar, "utf8").trim();
67
+ } catch {
68
+ return false;
69
+ }
70
+ if (!SHA256_RE.test(expected)) return false;
71
+ try {
72
+ return digestFile(cached) === expected;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ export async function fetchTo(url, dest, fetchImpl) {
79
+ let res;
80
+ try {
81
+ res = await fetchImpl(url, { redirect: "follow" });
82
+ } catch (err) {
83
+ const cause = err?.cause?.code ?? err?.code ?? err?.message ?? "unknown";
84
+ throw new Error(
85
+ `network error fetching ${url} (${cause}) — check your connection, or pin a release with WHITEMAGIC_RELEASE`,
86
+ );
87
+ }
88
+ if (!res.ok) {
89
+ throw new Error(
90
+ `HTTP ${res.status} fetching ${url} — that release asset may be missing; check https://github.com/${REPO}/releases`,
91
+ );
92
+ }
93
+ writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
94
+ }
95
+
96
+ /**
97
+ * Ensure a verified `wm` binary for this platform exists in the cache and
98
+ * return its path. Fresh downloads go temp -> checksum -> chmod -> rename;
99
+ * a cache entry that fails re-verification is replaced (self-healing).
100
+ */
101
+ export async function ensureBinary({
102
+ fetchImpl = fetch,
103
+ cacheRoot = defaultCacheRoot(),
104
+ tag,
105
+ pkgVersion,
106
+ p = platform(),
107
+ a = arch(),
108
+ log = console.error,
109
+ } = {}) {
110
+ const resolvedTag = tag ?? releaseTag(pkgVersion);
111
+ const asset = assetFor(p, a);
112
+ if (!asset) {
113
+ throw new Error(
114
+ `no release asset for ${p}/${a} yet — install from source: https://github.com/${REPO}#install`,
115
+ );
116
+ }
117
+ const cached = cachedBinaryPath(asset, resolvedTag, cacheRoot);
118
+ if (cacheIsValid(cached)) return cached;
119
+ if (existsSync(cached)) {
120
+ log("whitemagic-mcp: cached binary failed verification — re-downloading.");
121
+ }
122
+
123
+ log(`whitemagic-mcp: fetching ${resolvedTag}/${asset} ...`);
124
+ const binDir = join(cacheRoot, "whitemagic", "bin", resolvedTag);
125
+ mkdirSync(binDir, { recursive: true });
126
+ // Stage INSIDE the cache dir so the final rename is atomic (a /tmp staging
127
+ // dir can live on another filesystem, where rename fails with EXDEV).
128
+ const tmp = mkdtempSync(join(binDir, ".download-"));
129
+ try {
130
+ const tmpBin = join(tmp, asset);
131
+ const tmpSha = join(tmp, `${asset}.sha256`);
132
+ await fetchTo(`${BASE_URL(resolvedTag)}/${asset}`, tmpBin, fetchImpl);
133
+ await fetchTo(`${BASE_URL(resolvedTag)}/${asset}.sha256`, tmpSha, fetchImpl);
134
+ const expected = (readFileSync(tmpSha, "utf8").trim().split(/\s+/)[0] ?? "").toLowerCase();
135
+ if (!SHA256_RE.test(expected)) {
136
+ throw new Error(`release checksum for ${asset} is malformed — refusing to install it`);
137
+ }
138
+ const actual = digestFile(tmpBin);
139
+ if (actual !== expected) {
140
+ throw new Error(
141
+ `checksum mismatch for ${asset} (expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…) — the download may be corrupted; retry`,
142
+ );
143
+ }
144
+ chmodSync(tmpBin, 0o755);
145
+ renameSync(tmpBin, cached);
146
+ writeFileSync(sidecarFor(cached), `${expected}\n`);
147
+ return cached;
148
+ } finally {
149
+ rmSync(tmp, { recursive: true, force: true });
150
+ }
151
+ }
152
+
153
+ export function BASE_URL(tag) {
154
+ return `https://github.com/${REPO}/releases/download/${tag}`;
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whitemagic-mcp",
3
- "version": "9.1.6",
3
+ "version": "9.1.7",
4
4
  "description": "WhiteMagic MCP server \u2014 local-first memory and session continuity for AI coding agents. Installs and runs the wm binary; no telemetry by default, no cloud.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -30,5 +30,8 @@
30
30
  },
31
31
  "homepage": "https://whitemagic.dev",
32
32
  "bugs": "https://github.com/lbailey94/whitemagic/issues",
33
- "mcpName": "io.github.lbailey94/whitemagic-mcp"
33
+ "mcpName": "io.github.lbailey94/whitemagic-mcp",
34
+ "scripts": {
35
+ "test": "node --test"
36
+ }
34
37
  }