nothumanallowed 8.3.2 → 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.2",
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
 
@@ -42,7 +44,8 @@ export async function loadPlugin(name) {
42
44
  if (!fs.existsSync(pluginFile)) return null;
43
45
 
44
46
  try {
45
- const mod = await import(`file://${pluginFile}`);
47
+ const { pathToFileURL } = await import('url');
48
+ const mod = await import(pathToFileURL(pluginFile).href);
46
49
  return {
47
50
  card: mod.PLUGIN_CARD || { name: sanitized, version: '0.0.0', description: '', commands: [] },
48
51
  run: typeof mod.run === 'function' ? mod.run : null,
@@ -151,19 +154,69 @@ async function buildPluginContext(config) {
151
154
  };
152
155
  }
153
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
+
154
181
  // ── Registry (available plugins from server) ────────────────────────────────
155
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
+ */
156
188
  async function fetchRegistry() {
157
189
  try {
158
190
  const res = await fetch(PLUGINS_REGISTRY_URL, { signal: AbortSignal.timeout(10000) });
159
191
  if (!res.ok) return [];
160
192
  const data = await res.json();
161
- 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;
162
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 {}
163
208
  return [];
164
209
  }
165
210
  }
166
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
+
167
220
  // ── Plugin Template ─────────────────────────────────────────────────────────
168
221
 
169
222
  function generateTemplate(name) {
@@ -286,31 +339,61 @@ async function cmdInstall(name) {
286
339
  const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '').replace(/\.mjs$/, '');
287
340
  fs.mkdirSync(PLUGINS_DIR, { recursive: true });
288
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
289
359
  const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
290
360
  const url = `${BASE_URL}/plugins/${sanitized}.mjs`;
291
361
 
292
- info(`Installing plugin "${sanitized}" from ${url}...`);
293
-
362
+ info(`Downloading "${sanitized}" v${entry.version || '?'}...`);
294
363
  const success = await download(url, dest, { timeout: 15000 });
295
- if (success) {
296
- // Validate the downloaded plugin
297
- const plugin = await loadPlugin(sanitized);
298
- if (plugin && plugin.run) {
299
- ok(`Plugin "${sanitized}" installed to ~/.nha/plugins/`);
300
- if (plugin.card.description) {
301
- info(plugin.card.description);
302
- }
303
- if (plugin.card.commands && plugin.card.commands.length > 0) {
304
- info(`Commands: ${plugin.card.commands.join(', ')}`);
305
- }
306
- } else if (plugin) {
307
- warn(`Plugin "${sanitized}" installed but has no run() function.`);
308
- } else {
309
- warn(`Plugin "${sanitized}" downloaded but could not be loaded. Check the file.`);
310
- }
311
- } else {
364
+
365
+ if (!success) {
312
366
  fail(`Could not download plugin "${sanitized}".`);
313
- 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.`);
314
397
  }
315
398
  }
316
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.2';
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