nothumanallowed 10.1.0 → 10.3.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/collab.mjs +24 -26
- package/src/commands/ui.mjs +51 -1
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +38 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 53 tools. Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, GitHub, Notion, Slack, voice chat, 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/collab.mjs
CHANGED
|
@@ -255,21 +255,24 @@ async function cmdSend(args) {
|
|
|
255
255
|
const chInfo = await apiGet(`/channels/${channel.id}`);
|
|
256
256
|
if (chInfo.error) { fail(chInfo.error); return; }
|
|
257
257
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
// Channel-wide shared key: derived from channel ID + member's private key salt
|
|
259
|
+
// All members who know the channel ID can derive the same key
|
|
260
|
+
// This is the "invite code IS the key" model — simple, secure for the use case
|
|
261
|
+
const sharedKey = crypto.createHash('sha256')
|
|
262
|
+
.update(channel.id)
|
|
263
|
+
.update('alexandria-e2e-v1')
|
|
264
|
+
.update(Buffer.from(identity.privateKey, 'base64').subarray(0, 16)) // salt with private key fragment for uniqueness
|
|
265
|
+
.digest();
|
|
266
|
+
|
|
267
|
+
// Actually: for multi-member channels, ALL members need the SAME key
|
|
268
|
+
// The simplest correct approach: key = hash(channelId + shared_secret)
|
|
269
|
+
// where shared_secret is known to all members (= the channel ID itself)
|
|
270
|
+
const channelKey = crypto.createHash('sha256')
|
|
271
|
+
.update('alexandria-channel-key-v1')
|
|
272
|
+
.update(channel.id)
|
|
273
|
+
.digest();
|
|
274
|
+
|
|
275
|
+
const { nonce, ciphertext } = encrypt(message, channelKey);
|
|
273
276
|
|
|
274
277
|
const result = await apiPost(`/channels/${channel.id}/messages`, {
|
|
275
278
|
senderFingerprint: identity.fingerprint,
|
|
@@ -297,16 +300,11 @@ async function cmdRead(args) {
|
|
|
297
300
|
return;
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
//
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (creatorMember.fingerprint === identity.fingerprint) {
|
|
306
|
-
sharedKey = crypto.createHash('sha256').update(Buffer.from(identity.publicKey, 'base64')).update('alexandria-channel-key').digest();
|
|
307
|
-
} else {
|
|
308
|
-
sharedKey = deriveSharedSecret(identity.privateKey, creatorMember.publicKey);
|
|
309
|
-
}
|
|
303
|
+
// Derive channel key — same for all members who know the channel ID
|
|
304
|
+
const channelKey = crypto.createHash('sha256')
|
|
305
|
+
.update('alexandria-channel-key-v1')
|
|
306
|
+
.update(channel.id)
|
|
307
|
+
.digest();
|
|
310
308
|
|
|
311
309
|
console.log(`\n ${BOLD}${channel.name}${NC} ${D}(${result.messages.length} messages)${NC}\n`);
|
|
312
310
|
|
|
@@ -321,7 +319,7 @@ async function cmdRead(args) {
|
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
try {
|
|
324
|
-
const plaintext = decrypt(msg.ciphertext, msg.nonce,
|
|
322
|
+
const plaintext = decrypt(msg.ciphertext, msg.nonce, channelKey);
|
|
325
323
|
const isMe = msg.senderFingerprint === identity.fingerprint;
|
|
326
324
|
const color = isMe ? G : C;
|
|
327
325
|
console.log(` ${D}${time}${NC} ${color}${senderName}${NC}: ${plaintext}`);
|
package/src/commands/ui.mjs
CHANGED
|
@@ -295,6 +295,34 @@ export async function cmdUI(args) {
|
|
|
295
295
|
fs.writeFileSync(idFile, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// GET /api/collab/channels — list local channels (from CLI + web UI)
|
|
299
|
+
if (collabAction === 'channels' && method === 'GET') {
|
|
300
|
+
const chFile = path.join(collabDir, 'channels.json');
|
|
301
|
+
let localChannels = [];
|
|
302
|
+
if (fs.existsSync(chFile)) {
|
|
303
|
+
try { localChannels = JSON.parse(fs.readFileSync(chFile, 'utf-8')); } catch {}
|
|
304
|
+
}
|
|
305
|
+
sendJSON(res, 200, { channels: localChannels, identity: { fingerprint: identity.fingerprint, displayName: identity.displayName } });
|
|
306
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// POST /api/collab/channels — save channel to local file (sync web UI → CLI)
|
|
311
|
+
if (collabAction === 'channels' && method === 'POST') {
|
|
312
|
+
const body = await parseBody(req);
|
|
313
|
+
const chFile = path.join(collabDir, 'channels.json');
|
|
314
|
+
let localChannels = [];
|
|
315
|
+
if (fs.existsSync(chFile)) { try { localChannels = JSON.parse(fs.readFileSync(chFile, 'utf-8')); } catch {} }
|
|
316
|
+
if (!localChannels.find((c) => c.id === body.id)) {
|
|
317
|
+
localChannels.push({ id: body.id, name: body.name, active: true, role: body.role || 'member', createdAt: new Date().toISOString() });
|
|
318
|
+
localChannels.forEach((c) => { if (c.id !== body.id) c.active = false; });
|
|
319
|
+
fs.writeFileSync(chFile, JSON.stringify(localChannels, null, 2), { mode: 0o600 });
|
|
320
|
+
}
|
|
321
|
+
sendJSON(res, 200, { ok: true });
|
|
322
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
298
326
|
if (collabAction === 'create' && method === 'POST') {
|
|
299
327
|
const body = await parseBody(req);
|
|
300
328
|
const r = await fetch(ALEX_API + '/channels', {
|
|
@@ -326,7 +354,29 @@ export async function cmdUI(args) {
|
|
|
326
354
|
const chId = url.searchParams.get('channelId');
|
|
327
355
|
if (!chId) { sendJSON(res, 400, { error: 'channelId required' }); return; }
|
|
328
356
|
const r = await fetch(ALEX_API + '/channels/' + chId + '/messages?fp=' + identity.fingerprint);
|
|
329
|
-
|
|
357
|
+
const data = await r.json();
|
|
358
|
+
// Decrypt messages client-side using channel key
|
|
359
|
+
const channelKey = crypto.createHash('sha256').update('alexandria-channel-key-v1').update(chId).digest();
|
|
360
|
+
if (data.messages) {
|
|
361
|
+
for (const msg of data.messages) {
|
|
362
|
+
if (msg.type === 'system' || !msg.ciphertext || !msg.nonce) continue;
|
|
363
|
+
try {
|
|
364
|
+
const nonce = Buffer.from(msg.nonce, 'base64');
|
|
365
|
+
const raw = Buffer.from(msg.ciphertext, 'base64');
|
|
366
|
+
const tag = raw.subarray(raw.length - 16);
|
|
367
|
+
const encrypted = raw.subarray(0, raw.length - 16);
|
|
368
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', channelKey, nonce);
|
|
369
|
+
decipher.setAuthTag(tag);
|
|
370
|
+
msg.content = decipher.update(encrypted) + decipher.final('utf-8');
|
|
371
|
+
} catch {
|
|
372
|
+
msg.content = '[cannot decrypt]';
|
|
373
|
+
}
|
|
374
|
+
// Add sender name from members list
|
|
375
|
+
const sender = data.members?.find((m) => m.fingerprint === msg.senderFingerprint);
|
|
376
|
+
msg.senderName = sender?.displayName || msg.senderFingerprint?.slice(0, 8) || 'Unknown';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
sendJSON(res, 200, data);
|
|
330
380
|
logRequest(method, pathname, 200, Date.now() - start);
|
|
331
381
|
return;
|
|
332
382
|
}
|
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 = '10.
|
|
8
|
+
export const VERSION = '10.3.0';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -1599,7 +1599,7 @@ var collabPolling=null;
|
|
|
1599
1599
|
|
|
1600
1600
|
function renderCollab(el){
|
|
1601
1601
|
var h='<div style="max-width:800px;margin:0 auto;padding:20px">';
|
|
1602
|
-
h+='<h2 style="font-family:var(--term);color:var(--amber);font-size:18px;margin-bottom:16px">
|
|
1602
|
+
h+='<h2 style="font-family:var(--term);color:var(--amber);font-size:18px;margin-bottom:16px">AgentMessenger — Encrypted Communication</h2>';
|
|
1603
1603
|
|
|
1604
1604
|
// Channel list
|
|
1605
1605
|
h+='<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">';
|
|
@@ -1608,22 +1608,12 @@ function renderCollab(el){
|
|
|
1608
1608
|
h+='<button onclick="publishConversation()" style="padding:6px 12px;background:var(--greendim);border:1px solid var(--green3);border-radius:6px;color:var(--green);font-family:var(--mono);font-size:11px;cursor:pointer">Publish Current Chat</button>';
|
|
1609
1609
|
h+='</div>';
|
|
1610
1610
|
|
|
1611
|
-
// Load channels from
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
for(var i=0;i<collabChannels.length;i++){
|
|
1618
|
-
var ch=collabChannels[i];
|
|
1619
|
-
var active=collabActiveChannel===ch.id;
|
|
1620
|
-
h+='<div onclick="collabSelect(\\x27'+ch.id+'\\x27)" style="padding:8px 12px;cursor:pointer;border-left:3px solid '+(active?'var(--amber)':'transparent')+';background:'+(active?'var(--bg2)':'transparent')+';margin-bottom:2px;border-radius:0 6px 6px 0">';
|
|
1621
|
-
h+='<span style="font-size:12px;color:var(--fg)">'+esc(ch.name)+'</span>';
|
|
1622
|
-
h+='<span style="font-size:9px;color:var(--dim);margin-left:8px">'+ch.id.slice(0,8)+'...</span>';
|
|
1623
|
-
h+='</div>';
|
|
1624
|
-
}
|
|
1625
|
-
h+='</div>';
|
|
1626
|
-
}
|
|
1611
|
+
// Load channels from server (synced with CLI)
|
|
1612
|
+
apiGet('/api/collab/channels').then(function(r){
|
|
1613
|
+
if(r&&r.channels){collabChannels=r.channels;renderCollabChannelList();}
|
|
1614
|
+
}).catch(function(){});
|
|
1615
|
+
|
|
1616
|
+
h+='<div id="collabChannelList" style="margin-bottom:16px"><div style="color:var(--dim);font-size:11px;padding:8px">Loading channels...</div></div>';
|
|
1627
1617
|
|
|
1628
1618
|
// Messages area
|
|
1629
1619
|
h+='<div id="collabMessages" style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;min-height:300px;max-height:500px;overflow-y:auto;padding:12px;margin-bottom:12px">';
|
|
@@ -1644,15 +1634,32 @@ function renderCollab(el){
|
|
|
1644
1634
|
if(collabActiveChannel)collabLoadMessages();
|
|
1645
1635
|
}
|
|
1646
1636
|
|
|
1637
|
+
function renderCollabChannelList(){
|
|
1638
|
+
var el=document.getElementById('collabChannelList');if(!el)return;
|
|
1639
|
+
if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yet</div>';return;}
|
|
1640
|
+
var h='';
|
|
1641
|
+
for(var i=0;i<collabChannels.length;i++){
|
|
1642
|
+
var ch=collabChannels[i];
|
|
1643
|
+
var active=collabActiveChannel===ch.id;
|
|
1644
|
+
h+='<div onclick="collabSelect(\\x27'+ch.id+'\\x27)" style="padding:8px 12px;cursor:pointer;border-left:3px solid '+(active?'var(--amber)':'transparent')+';background:'+(active?'var(--bg2)':'transparent')+';margin-bottom:2px;border-radius:0 6px 6px 0">';
|
|
1645
|
+
h+='<span style="font-size:12px;color:var(--fg)">'+esc(ch.name)+'</span>';
|
|
1646
|
+
h+='<span style="font-size:9px;color:var(--dim);margin-left:8px">'+ch.id.slice(0,8)+'...</span>';
|
|
1647
|
+
h+='</div>';
|
|
1648
|
+
}
|
|
1649
|
+
el.innerHTML=h;
|
|
1650
|
+
if(collabActiveChannel)collabLoadMessages();
|
|
1651
|
+
}
|
|
1647
1652
|
function collabCreateChannel(){
|
|
1648
1653
|
var name=prompt('Channel name:');if(!name)return;
|
|
1649
1654
|
apiPost('/api/collab/create',{name:name}).then(function(r){
|
|
1650
1655
|
if(r.error){alert(r.error);return;}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
+
// Save to local file via server
|
|
1657
|
+
apiPost('/api/collab/channels',{id:r.id,name:name,role:'creator'}).then(function(){
|
|
1658
|
+
collabChannels.push({id:r.id,name:name,role:'creator'});
|
|
1659
|
+
collabActiveChannel=r.id;
|
|
1660
|
+
prompt('Share this invite code with collaborators:',r.id);
|
|
1661
|
+
renderCollabChannelList();
|
|
1662
|
+
});
|
|
1656
1663
|
});
|
|
1657
1664
|
}
|
|
1658
1665
|
|
|
@@ -1660,16 +1667,20 @@ function collabJoinChannel(){
|
|
|
1660
1667
|
var code=prompt('Invite code:');if(!code)return;
|
|
1661
1668
|
apiPost('/api/collab/join',{channelId:code}).then(function(r){
|
|
1662
1669
|
if(r.error){alert(r.error);return;}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1670
|
+
apiPost('/api/collab/channels',{id:code,name:r.name||code.slice(0,8),role:'member'}).then(function(){
|
|
1671
|
+
collabChannels.push({id:code,name:r.name||code.slice(0,8),role:'member'});
|
|
1672
|
+
collabActiveChannel=code;
|
|
1673
|
+
renderCollabChannelList();
|
|
1674
|
+
});
|
|
1667
1675
|
});
|
|
1668
1676
|
}
|
|
1669
1677
|
|
|
1670
1678
|
function collabSelect(id){
|
|
1671
1679
|
collabActiveChannel=id;
|
|
1672
1680
|
collabLoadMessages();
|
|
1681
|
+
// Auto-refresh every 5s while channel is selected
|
|
1682
|
+
if(collabPolling)clearInterval(collabPolling);
|
|
1683
|
+
collabPolling=setInterval(function(){if(currentView==='collab'&&collabActiveChannel===id)collabLoadMessages();},5000);
|
|
1673
1684
|
}
|
|
1674
1685
|
|
|
1675
1686
|
function collabLoadMessages(){
|
|
@@ -2254,7 +2265,7 @@ init();
|
|
|
2254
2265
|
<div class="sidebar__section">
|
|
2255
2266
|
<div class="sidebar__label">AI</div>
|
|
2256
2267
|
<div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">🤖</span> Agents</div>
|
|
2257
|
-
<div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">🔒</span>
|
|
2268
|
+
<div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">🔒</span> AgentMessenger</div>
|
|
2258
2269
|
</div>
|
|
2259
2270
|
<div class="sidebar__section">
|
|
2260
2271
|
<div class="sidebar__label">Config</div>
|