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 +1 -1
- package/src/commands/plugin.mjs +103 -21
- package/src/constants.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "8.
|
|
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": {
|
package/src/commands/plugin.mjs
CHANGED
|
@@ -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
|
-
|
|
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(`
|
|
294
|
-
|
|
362
|
+
info(`Downloading "${sanitized}" v${entry.version || '?'}...`);
|
|
295
363
|
const success = await download(url, dest, { timeout: 15000 });
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|