nothumanallowed 8.3.3 → 8.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "8.3.3",
3
+ "version": "8.4.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents + unified productivity suite. Gmail, Calendar, Drive, Contacts, Tasks, GitHub, Notion, Slack, voice chat, smart scheduler. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
 
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
+ import crypto from 'crypto';
22
23
  import { loadConfig } from '../config.mjs';
23
24
  import { callLLM, callAgent } from '../services/llm.mjs';
24
25
  import { NHA_DIR, PLUGINS_DIR, BASE_URL, VERSION } from '../constants.mjs';
@@ -28,6 +29,7 @@ import { info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
28
29
  // ── Constants ──────────────────────────────────────────────────────────────
29
30
 
30
31
  const PLUGINS_REGISTRY_URL = `${BASE_URL}/plugins/registry.json`;
32
+ const LOCAL_REGISTRY_FILE = path.join(PLUGINS_DIR, '.registry.json');
31
33
 
32
34
  // ── Plugin Loader ──────────────────────────────────────────────────────────
33
35
 
@@ -152,19 +154,69 @@ async function buildPluginContext(config) {
152
154
  };
153
155
  }
154
156
 
157
+ // ── SHA-256 Integrity Verification ───────────────────────────────────────────
158
+
159
+ /**
160
+ * Compute SHA-256 hash of a file.
161
+ * @param {string} filePath
162
+ * @returns {string} hex hash
163
+ */
164
+ function computeSHA256(filePath) {
165
+ const content = fs.readFileSync(filePath);
166
+ return crypto.createHash('sha256').update(content).digest('hex');
167
+ }
168
+
169
+ /**
170
+ * Verify a plugin file against its registry SHA-256 hash.
171
+ * @param {string} filePath — path to the downloaded plugin
172
+ * @param {string} expectedHash — SHA-256 hex from registry
173
+ * @returns {boolean}
174
+ */
175
+ function verifyIntegrity(filePath, expectedHash) {
176
+ if (!expectedHash) return false;
177
+ const actual = computeSHA256(filePath);
178
+ return actual === expectedHash;
179
+ }
180
+
155
181
  // ── Registry (available plugins from server) ────────────────────────────────
156
182
 
183
+ /**
184
+ * Fetch the plugin registry from the server.
185
+ * Registry format: { plugins: [{ name, version, description, sha256, size, author }] }
186
+ * The sha256 field is the hex hash of the .mjs file — verified on install.
187
+ */
157
188
  async function fetchRegistry() {
158
189
  try {
159
190
  const res = await fetch(PLUGINS_REGISTRY_URL, { signal: AbortSignal.timeout(10000) });
160
191
  if (!res.ok) return [];
161
192
  const data = await res.json();
162
- return Array.isArray(data.plugins) ? data.plugins : [];
193
+ const plugins = Array.isArray(data.plugins) ? data.plugins : [];
194
+
195
+ // Cache registry locally for offline reference
196
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
197
+ fs.writeFileSync(LOCAL_REGISTRY_FILE, JSON.stringify(data, null, 2), 'utf-8');
198
+
199
+ return plugins;
163
200
  } catch {
201
+ // Fallback to local cached registry
202
+ try {
203
+ if (fs.existsSync(LOCAL_REGISTRY_FILE)) {
204
+ const data = JSON.parse(fs.readFileSync(LOCAL_REGISTRY_FILE, 'utf-8'));
205
+ return Array.isArray(data.plugins) ? data.plugins : [];
206
+ }
207
+ } catch {}
164
208
  return [];
165
209
  }
166
210
  }
167
211
 
212
+ /**
213
+ * Get the registry entry for a plugin by name.
214
+ */
215
+ async function getRegistryEntry(name) {
216
+ const registry = await fetchRegistry();
217
+ return registry.find(p => p.name === name) || null;
218
+ }
219
+
168
220
  // ── Plugin Template ─────────────────────────────────────────────────────────
169
221
 
170
222
  function generateTemplate(name) {
@@ -287,31 +339,61 @@ async function cmdInstall(name) {
287
339
  const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '').replace(/\.mjs$/, '');
288
340
  fs.mkdirSync(PLUGINS_DIR, { recursive: true });
289
341
 
342
+ // Step 1: Check registry for SHA-256 hash
343
+ info(`Checking registry for "${sanitized}"...`);
344
+ const entry = await getRegistryEntry(sanitized);
345
+
346
+ if (!entry) {
347
+ fail(`Plugin "${sanitized}" not found in registry.`);
348
+ info('Available plugins: nha plugin list');
349
+ info(`Or create your own: nha plugin create ${sanitized}`);
350
+ return;
351
+ }
352
+
353
+ if (!entry.sha256) {
354
+ fail(`Plugin "${sanitized}" has no integrity hash in registry. Aborting for security.`);
355
+ return;
356
+ }
357
+
358
+ // Step 2: Download the plugin
290
359
  const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
291
360
  const url = `${BASE_URL}/plugins/${sanitized}.mjs`;
292
361
 
293
- info(`Installing plugin "${sanitized}" from ${url}...`);
294
-
362
+ info(`Downloading "${sanitized}" v${entry.version || '?'}...`);
295
363
  const success = await download(url, dest, { timeout: 15000 });
296
- if (success) {
297
- // Validate the downloaded plugin
298
- const plugin = await loadPlugin(sanitized);
299
- if (plugin && plugin.run) {
300
- ok(`Plugin "${sanitized}" installed to ~/.nha/plugins/`);
301
- if (plugin.card.description) {
302
- info(plugin.card.description);
303
- }
304
- if (plugin.card.commands && plugin.card.commands.length > 0) {
305
- info(`Commands: ${plugin.card.commands.join(', ')}`);
306
- }
307
- } else if (plugin) {
308
- warn(`Plugin "${sanitized}" installed but has no run() function.`);
309
- } else {
310
- warn(`Plugin "${sanitized}" downloaded but could not be loaded. Check the file.`);
311
- }
312
- } else {
364
+
365
+ if (!success) {
313
366
  fail(`Could not download plugin "${sanitized}".`);
314
- info(`Try: nha plugin create ${sanitized} (to create it locally)`);
367
+ return;
368
+ }
369
+
370
+ // Step 3: Verify SHA-256 integrity
371
+ info('Verifying SHA-256 integrity...');
372
+ const isValid = verifyIntegrity(dest, entry.sha256);
373
+
374
+ if (!isValid) {
375
+ // CRITICAL: hash mismatch — file may be tampered
376
+ fs.rmSync(dest, { force: true });
377
+ fail(`INTEGRITY CHECK FAILED for "${sanitized}"!`);
378
+ fail(`Expected SHA-256: ${entry.sha256}`);
379
+ fail(`Got SHA-256: ${computeSHA256(dest)}`);
380
+ fail('The downloaded file does not match the registry hash. File deleted for safety.');
381
+ fail('This could indicate a compromised server or man-in-the-middle attack.');
382
+ return;
383
+ }
384
+
385
+ ok(`SHA-256 verified: ${entry.sha256.slice(0, 16)}...`);
386
+
387
+ // Step 4: Load and validate the plugin
388
+ const plugin = await loadPlugin(sanitized);
389
+ if (plugin && plugin.run) {
390
+ ok(`Plugin "${sanitized}" v${entry.version || plugin.card.version} installed.`);
391
+ if (plugin.card.description) info(plugin.card.description);
392
+ if (plugin.card.commands?.length > 0) info(`Commands: ${plugin.card.commands.join(', ')}`);
393
+ } else if (plugin) {
394
+ warn(`Plugin "${sanitized}" installed but has no run() function.`);
395
+ } else {
396
+ warn(`Plugin "${sanitized}" downloaded but could not be loaded.`);
315
397
  }
316
398
  }
317
399
 
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '8.3.3';
8
+ export const VERSION = '8.4.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11