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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "10.1.0",
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": {
@@ -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
- // For simplicity, use a channel-wide shared key derived from creator's public key + our private key
259
- // In production, you'd do pairwise key exchange for each member
260
- const creatorMember = chInfo.members[0]; // First member is creator
261
- if (!creatorMember) { fail('Channel has no members'); return; }
262
-
263
- let sharedKey;
264
- if (creatorMember.fingerprint === identity.fingerprint) {
265
- // We're the creator — use a key derived from our own keypair (self-encryption)
266
- // Other members will derive the same key using our public key
267
- sharedKey = crypto.createHash('sha256').update(Buffer.from(identity.publicKey, 'base64')).update('alexandria-channel-key').digest();
268
- } else {
269
- sharedKey = deriveSharedSecret(identity.privateKey, creatorMember.publicKey);
270
- }
271
-
272
- const { nonce, ciphertext } = encrypt(message, sharedKey);
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
- // Get channel info for key exchange
301
- const chInfo = await apiGet(`/channels/${channel.id}`);
302
- const creatorMember = chInfo.members[0];
303
-
304
- let sharedKey;
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, sharedKey);
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}`);
@@ -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
- sendJSON(res, 200, await r.json());
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.1.0';
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
 
@@ -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">Alexandria — Encrypted Communication</h2>';
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 localStorage
1612
- var saved=[];try{saved=JSON.parse(localStorage.getItem('nha_collab_channels')||'[]');}catch(e){}
1613
- collabChannels=saved;
1614
-
1615
- if(collabChannels.length>0){
1616
- h+='<div style="margin-bottom:16px">';
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
- collabChannels.push({id:r.id,name:name});
1652
- collabActiveChannel=r.id;
1653
- localStorage.setItem('nha_collab_channels',JSON.stringify(collabChannels));
1654
- prompt('Share this invite code:',r.id);
1655
- renderCollab(document.getElementById('content'));
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
- collabChannels.push({id:code,name:r.name||code.slice(0,8)});
1664
- collabActiveChannel=code;
1665
- localStorage.setItem('nha_collab_channels',JSON.stringify(collabChannels));
1666
- renderCollab(document.getElementById('content'));
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">&#129302;</span> Agents</div>
2257
- <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> Collab</div>
2268
+ <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> AgentMessenger</div>
2258
2269
  </div>
2259
2270
  <div class="sidebar__section">
2260
2271
  <div class="sidebar__label">Config</div>