From 948c099418f7d779e89e65acdd36b34c6de93da1 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Tue, 14 Apr 2026 12:42:38 -0500 Subject: [PATCH] fix: validate version field is valid semver Non-semver version strings (e.g., "2024.03.31.01") pass validation today but cause fatal unrecoverable crashes in Claude Desktop at bootstrap. Add semver validation to catch these before pack/distribution. Fixes #226 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 ++ src/node/validate.ts | 30 +++++++++++++++++++++ test/validate.test.ts | 62 +++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 18 +++++++++++++ 4 files changed, 112 insertions(+) diff --git a/package.json b/package.json index 09a33ee..7c61e22 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.15.3", "@types/node-forge": "1.3.11", + "@types/semver": "^7.7.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.43.0", @@ -91,6 +92,7 @@ "ignore": "^7.0.5", "node-forge": "^1.3.2", "pretty-bytes": "^5.6.0", + "semver": "^7.7.4", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.6" }, diff --git a/src/node/validate.ts b/src/node/validate.ts index b958798..b8ae4f2 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -4,6 +4,7 @@ import { DestroyerOfModules } from "galactus"; import * as os from "os"; import { dirname, extname, isAbsolute, join, resolve } from "path"; import prettyBytes from "pretty-bytes"; +import semver from "semver"; import { unpackExtension } from "../cli/unpack.js"; import { @@ -238,6 +239,25 @@ function validateCommandVariables(manifest: { return { valid: errors.length === 0, errors, warnings }; } +/** + * Validate that the version field is a valid semantic version. + * Non-semver versions cause fatal crashes in Claude Desktop. + */ +function validateVersion(version: string): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + if (!semver.valid(version)) { + errors.push( + `Version "${version}" is not valid semver. ` + + `Expected format: MAJOR.MINOR.PATCH (e.g., "1.0.0", "2.1.0-beta.1"). ` + + `Non-semver versions cause fatal crashes in Claude Desktop.`, + ); + } + + return { valid: errors.length === 0, errors, warnings }; +} + // Sensitive file patterns not already covered by EXCLUDE_PATTERNS in files.ts const SENSITIVE_PATTERNS = [ /(^|\/)credentials\.json$/i, @@ -323,6 +343,16 @@ export function validateManifest( : manifestDir; let hasErrors = false; + // Validate version format (non-semver crashes Claude Desktop) + const versionValidation = validateVersion(manifestData.version); + if (versionValidation.errors.length > 0) { + console.log("\nERROR: Version validation failed:\n"); + versionValidation.errors.forEach((error) => { + console.log(` - ${error}`); + }); + hasErrors = true; + } + // Validate icon if present (always relative to manifest directory) if (manifestData.icon) { const iconValidation = validateIcon(manifestData.icon, manifestDir); diff --git a/test/validate.test.ts b/test/validate.test.ts index e998fb2..f7bb60e 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -350,6 +350,68 @@ describe("Enhanced Validation", () => { }); }); + describe("version validation", () => { + it("should reject non-semver calver version", () => { + const dir = join(fixturesDir, "version-calver"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { version: "2024.03.31.01" }); + + try { + execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" }); + fail("Expected validation to fail"); + } catch (e) { + const error = e as ExecSyncError; + expect(error.status).toBe(1); + expect(error.stdout.toString()).toContain("not valid semver"); + } + }); + + it("should reject non-numeric version string", () => { + const dir = join(fixturesDir, "version-string"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { version: "latest" }); + + try { + execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" }); + fail("Expected validation to fail"); + } catch (e) { + const error = e as ExecSyncError; + expect(error.status).toBe(1); + expect(error.stdout.toString()).toContain("not valid semver"); + } + }); + + it("should accept standard semver version", () => { + const dir = join(fixturesDir, "version-semver"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { version: "1.0.0" }); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).not.toContain("not valid semver"); + }); + + it("should accept semver with prerelease and build metadata", () => { + const dir = join(fixturesDir, "version-prerelease"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { version: "2.1.0-beta.1+build.123" }); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).not.toContain("not valid semver"); + }); + }); + describe("happy path", () => { it("should pass with all files present and correct types", () => { const dir = join(fixturesDir, "happy-path"); diff --git a/yarn.lock b/yarn.lock index 538dc22..8dcb46a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,7 @@ __metadata: "@types/jest": "npm:^29.5.14" "@types/node": "npm:^22.15.3" "@types/node-forge": "npm:1.3.11" + "@types/semver": "npm:^7.7.1" "@typescript-eslint/eslint-plugin": "npm:^5.62.0" "@typescript-eslint/parser": "npm:^5.62.0" commander: "npm:^13.1.0" @@ -39,6 +40,7 @@ __metadata: node-forge: "npm:^1.3.2" prettier: "npm:^3.3.3" pretty-bytes: "npm:^5.6.0" + semver: "npm:^7.7.4" ts-jest: "npm:^29.3.2" typescript: "npm:^5.6.3" zod: "npm:^3.25.67" @@ -1257,6 +1259,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.7.1": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -5394,6 +5403,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.4": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2"