semantiq-mcp 0.8.0 → 0.9.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 (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/install.js +98 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semantiq-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Semantic code understanding for AI tools - One MCP Server for all AI coding assistants",
5
5
  "bin": {
6
6
  "semantiq": "bin/semantiq"
@@ -3,6 +3,7 @@
3
3
  const https = require('https');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const crypto = require('crypto');
6
7
  const { execSync } = require('child_process');
7
8
 
8
9
  const VERSION = require('../package.json').version;
@@ -36,6 +37,11 @@ function downloadFile(url, dest) {
36
37
  return new Promise((resolve, reject) => {
37
38
  const file = fs.createWriteStream(dest);
38
39
 
40
+ const cleanup = () => {
41
+ file.close();
42
+ fs.unlink(dest, () => {});
43
+ };
44
+
39
45
  const request = (url) => {
40
46
  https.get(url, (response) => {
41
47
  if (response.statusCode === 302 || response.statusCode === 301) {
@@ -44,6 +50,7 @@ function downloadFile(url, dest) {
44
50
  }
45
51
 
46
52
  if (response.statusCode !== 200) {
53
+ cleanup();
47
54
  reject(new Error(`Failed to download: ${response.statusCode}`));
48
55
  return;
49
56
  }
@@ -53,6 +60,43 @@ function downloadFile(url, dest) {
53
60
  file.close();
54
61
  resolve();
55
62
  });
63
+ response.on('error', (err) => {
64
+ cleanup();
65
+ reject(err);
66
+ });
67
+ }).on('error', (err) => {
68
+ cleanup();
69
+ reject(err);
70
+ });
71
+ };
72
+
73
+ request(url);
74
+ });
75
+ }
76
+
77
+ // Download a small text resource (the .sha256 file) directly into memory,
78
+ // following redirects. Rejects on any non-200 final status.
79
+ function downloadText(url) {
80
+ return new Promise((resolve, reject) => {
81
+ const request = (url) => {
82
+ https.get(url, (response) => {
83
+ if (response.statusCode === 302 || response.statusCode === 301) {
84
+ request(response.headers.location);
85
+ return;
86
+ }
87
+
88
+ if (response.statusCode !== 200) {
89
+ reject(new Error(`Failed to download checksum: ${response.statusCode}`));
90
+ return;
91
+ }
92
+
93
+ let data = '';
94
+ response.setEncoding('utf8');
95
+ response.on('data', (chunk) => {
96
+ data += chunk;
97
+ });
98
+ response.on('end', () => resolve(data));
99
+ response.on('error', reject);
56
100
  }).on('error', reject);
57
101
  };
58
102
 
@@ -60,22 +104,74 @@ function downloadFile(url, dest) {
60
104
  });
61
105
  }
62
106
 
107
+ function sha256File(filePath) {
108
+ const hash = crypto.createHash('sha256');
109
+ hash.update(fs.readFileSync(filePath));
110
+ return hash.digest('hex');
111
+ }
112
+
113
+ // The published .sha256 may be either a bare hex digest or the standard
114
+ // `sha256sum` format: "<hex> <filename>". Extract the leading hex token.
115
+ function parseExpectedSha256(raw, archiveName) {
116
+ const tokens = raw.trim().split(/\s+/);
117
+ const hex = (tokens[0] || '').toLowerCase();
118
+ if (!/^[0-9a-f]{64}$/.test(hex)) {
119
+ throw new Error(`Malformed checksum file for ${archiveName}`);
120
+ }
121
+ return hex;
122
+ }
123
+
63
124
  async function install() {
64
125
  const { target, isWindows } = getPlatform();
65
126
  const binName = isWindows ? 'semantiq.exe' : 'semantiq';
66
127
  const archiveName = `semantiq-v${VERSION}-${target}.tar.gz`;
67
128
  const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
129
+ const checksumUrl = `${url}.sha256`;
68
130
 
69
131
  const binDir = path.join(__dirname, '..', 'bin');
70
132
  const binPath = path.join(binDir, binName);
71
133
  const archivePath = path.join(binDir, archiveName);
72
134
 
135
+ // Best-effort removal of partial/untrusted artifacts.
136
+ const removeArtifacts = () => {
137
+ for (const p of [archivePath, binPath]) {
138
+ try {
139
+ if (fs.existsSync(p)) fs.unlinkSync(p);
140
+ } catch (_) {
141
+ // ignore
142
+ }
143
+ }
144
+ };
145
+
73
146
  console.log(`Downloading Semantiq v${VERSION} for ${target}...`);
74
147
 
75
148
  try {
76
149
  await downloadFile(url, archivePath);
77
150
 
78
- // Extract
151
+ // Integrity verification: never extract/execute an unverified binary.
152
+ // The release CI publishes "<archive>.sha256" next to each artifact.
153
+ let expectedSha;
154
+ try {
155
+ const checksumRaw = await downloadText(checksumUrl);
156
+ expectedSha = parseExpectedSha256(checksumRaw, archiveName);
157
+ } catch (err) {
158
+ removeArtifacts();
159
+ throw new Error(
160
+ `Could not verify integrity (missing or unreadable ${archiveName}.sha256): ${err.message}`
161
+ );
162
+ }
163
+
164
+ const actualSha = sha256File(archivePath);
165
+ if (actualSha !== expectedSha) {
166
+ removeArtifacts();
167
+ throw new Error(
168
+ `Checksum mismatch for ${archiveName}\n expected: ${expectedSha}\n actual: ${actualSha}`
169
+ );
170
+ }
171
+
172
+ console.log('Checksum verified.');
173
+
174
+ // Extract (verified archive only)
79
175
  if (isWindows) {
80
176
  execSync(`tar -xzf "${archivePath}" -C "${binDir}"`, { stdio: 'inherit' });
81
177
  } else {
@@ -88,6 +184,7 @@ async function install() {
88
184
 
89
185
  console.log('Semantiq installed successfully!');
90
186
  } catch (error) {
187
+ removeArtifacts();
91
188
  console.error('Failed to install Semantiq:', error.message);
92
189
  console.error('');
93
190
  console.error('Alternative installation methods:');