nothumanallowed 9.9.6 → 10.0.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/cli.mjs +12 -0
- package/src/commands/collab.mjs +436 -0
- package/src/constants.mjs +1 -1
- package/src/services/ops-daemon.mjs +11 -3
- package/src/services/web-ui.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.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/cli.mjs
CHANGED
|
@@ -106,6 +106,12 @@ export async function main(argv) {
|
|
|
106
106
|
// Alias for nha ops (friendlier name)
|
|
107
107
|
return cmdOps(args.length ? args : ['start']);
|
|
108
108
|
|
|
109
|
+
case 'collab':
|
|
110
|
+
case 'alexandria': {
|
|
111
|
+
const { cmdCollab } = await import('./commands/collab.mjs');
|
|
112
|
+
return cmdCollab(args);
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
case 'plugin':
|
|
110
116
|
case 'plugins':
|
|
111
117
|
return cmdPlugin(args);
|
|
@@ -657,6 +663,12 @@ function cmdHelp() {
|
|
|
657
663
|
console.log(` autostart disable Remove OS autostart`);
|
|
658
664
|
console.log(` autostart status Check autostart configuration\n`);
|
|
659
665
|
|
|
666
|
+
console.log(` ${C}Alexandria${NC} ${D}(E2E Encrypted Communication)${NC}`);
|
|
667
|
+
console.log(` collab create "name" Create encrypted channel`);
|
|
668
|
+
console.log(` collab join <code> Join with invite code`);
|
|
669
|
+
console.log(` collab send "msg" Encrypt and send message`);
|
|
670
|
+
console.log(` collab read Decrypt and show messages`);
|
|
671
|
+
console.log(` collab list Your channels\n`);
|
|
660
672
|
console.log(` ${C}Message Responder${NC} ${D}(Telegram + Discord auto-reply)${NC}`);
|
|
661
673
|
console.log(` responder status Show responder configuration`);
|
|
662
674
|
console.log(` config set telegram-bot-token TOKEN`);
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nha collab — Alexandria E2E Encrypted Inter-Agent Communication
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* nha collab create "name" — create encrypted channel
|
|
6
|
+
* nha collab join <code> — join channel with invite code
|
|
7
|
+
* nha collab send "message" — encrypt and send
|
|
8
|
+
* nha collab read — decrypt and show messages
|
|
9
|
+
* nha collab list — list your channels
|
|
10
|
+
* nha collab members — show channel members
|
|
11
|
+
* nha collab delete — delete channel (creator only)
|
|
12
|
+
*
|
|
13
|
+
* Crypto: X25519 key exchange + AES-256-GCM
|
|
14
|
+
* Identity: keypair stored in ~/.nha/collab/identity.json
|
|
15
|
+
* Zero-knowledge: server sees only ciphertext
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import crypto from 'crypto';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { NHA_DIR, API_BASE } from '../constants.mjs';
|
|
22
|
+
import { info, ok, fail, warn, C, G, D, Y, R, NC, BOLD } from '../ui.mjs';
|
|
23
|
+
|
|
24
|
+
const COLLAB_DIR = path.join(NHA_DIR, 'collab');
|
|
25
|
+
const IDENTITY_FILE = path.join(COLLAB_DIR, 'identity.json');
|
|
26
|
+
const CHANNELS_FILE = path.join(COLLAB_DIR, 'channels.json');
|
|
27
|
+
const API = API_BASE + '/alexandria';
|
|
28
|
+
|
|
29
|
+
// ── Crypto Primitives ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate X25519 keypair for Diffie-Hellman key exchange.
|
|
33
|
+
*/
|
|
34
|
+
function generateKeypair() {
|
|
35
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519', {
|
|
36
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
37
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
publicKey: publicKey.toString('base64'),
|
|
41
|
+
privateKey: privateKey.toString('base64'),
|
|
42
|
+
fingerprint: crypto.createHash('sha256').update(publicKey).digest('hex').slice(0, 16),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Derive shared secret from my private key + their public key (X25519 ECDH).
|
|
48
|
+
*/
|
|
49
|
+
function deriveSharedSecret(myPrivateKeyB64, theirPublicKeyB64) {
|
|
50
|
+
const myPrivate = crypto.createPrivateKey({
|
|
51
|
+
key: Buffer.from(myPrivateKeyB64, 'base64'),
|
|
52
|
+
format: 'der',
|
|
53
|
+
type: 'pkcs8',
|
|
54
|
+
});
|
|
55
|
+
const theirPublic = crypto.createPublicKey({
|
|
56
|
+
key: Buffer.from(theirPublicKeyB64, 'base64'),
|
|
57
|
+
format: 'der',
|
|
58
|
+
type: 'spki',
|
|
59
|
+
});
|
|
60
|
+
const shared = crypto.diffieHellman({
|
|
61
|
+
privateKey: myPrivate,
|
|
62
|
+
publicKey: theirPublic,
|
|
63
|
+
});
|
|
64
|
+
// Derive a 256-bit AES key from the shared secret
|
|
65
|
+
return crypto.createHash('sha256').update(shared).digest();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Encrypt a message with AES-256-GCM.
|
|
70
|
+
*/
|
|
71
|
+
function encrypt(plaintext, key) {
|
|
72
|
+
const nonce = crypto.randomBytes(12);
|
|
73
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
74
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
75
|
+
const tag = cipher.getAuthTag();
|
|
76
|
+
return {
|
|
77
|
+
nonce: nonce.toString('base64'),
|
|
78
|
+
ciphertext: Buffer.concat([encrypted, tag]).toString('base64'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Decrypt a message with AES-256-GCM.
|
|
84
|
+
*/
|
|
85
|
+
function decrypt(ciphertextB64, nonceB64, key) {
|
|
86
|
+
const nonce = Buffer.from(nonceB64, 'base64');
|
|
87
|
+
const data = Buffer.from(ciphertextB64, 'base64');
|
|
88
|
+
const tag = data.subarray(data.length - 16);
|
|
89
|
+
const encrypted = data.subarray(0, data.length - 16);
|
|
90
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
91
|
+
decipher.setAuthTag(tag);
|
|
92
|
+
return decipher.update(encrypted) + decipher.final('utf-8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Identity Management ───────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function loadIdentity() {
|
|
98
|
+
if (!fs.existsSync(IDENTITY_FILE)) return null;
|
|
99
|
+
try { return JSON.parse(fs.readFileSync(IDENTITY_FILE, 'utf-8')); } catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function saveIdentity(identity) {
|
|
103
|
+
fs.mkdirSync(COLLAB_DIR, { recursive: true });
|
|
104
|
+
fs.writeFileSync(IDENTITY_FILE, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getOrCreateIdentity() {
|
|
108
|
+
let id = loadIdentity();
|
|
109
|
+
if (!id) {
|
|
110
|
+
id = generateKeypair();
|
|
111
|
+
id.displayName = `Agent-${id.fingerprint.slice(0, 6)}`;
|
|
112
|
+
id.createdAt = new Date().toISOString();
|
|
113
|
+
saveIdentity(id);
|
|
114
|
+
info(`Identity created: ${id.fingerprint}`);
|
|
115
|
+
}
|
|
116
|
+
return id;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Channel Management ────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function loadChannels() {
|
|
122
|
+
if (!fs.existsSync(CHANNELS_FILE)) return [];
|
|
123
|
+
try { return JSON.parse(fs.readFileSync(CHANNELS_FILE, 'utf-8')); } catch { return []; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function saveChannels(channels) {
|
|
127
|
+
fs.mkdirSync(COLLAB_DIR, { recursive: true });
|
|
128
|
+
fs.writeFileSync(CHANNELS_FILE, JSON.stringify(channels, null, 2), { mode: 0o600 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getActiveChannel() {
|
|
132
|
+
const channels = loadChannels();
|
|
133
|
+
const active = channels.find(c => c.active);
|
|
134
|
+
return active || channels[0] || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setActiveChannel(channelId) {
|
|
138
|
+
const channels = loadChannels();
|
|
139
|
+
for (const ch of channels) ch.active = ch.id === channelId;
|
|
140
|
+
saveChannels(channels);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── API Helpers ───────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function apiPost(endpoint, body) {
|
|
146
|
+
const res = await fetch(API + endpoint, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
});
|
|
151
|
+
return res.json();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function apiGet(endpoint) {
|
|
155
|
+
const res = await fetch(API + endpoint);
|
|
156
|
+
return res.json();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function apiDelete(endpoint, body) {
|
|
160
|
+
const res = await fetch(API + endpoint, {
|
|
161
|
+
method: 'DELETE',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify(body),
|
|
164
|
+
});
|
|
165
|
+
return res.json();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async function cmdCreate(args) {
|
|
171
|
+
const name = args.join(' ') || 'Unnamed Channel';
|
|
172
|
+
const identity = getOrCreateIdentity();
|
|
173
|
+
|
|
174
|
+
// Encrypt channel name with a key derived from the channel (will be re-encrypted with shared key)
|
|
175
|
+
const result = await apiPost('/channels', {
|
|
176
|
+
name: name, // Sent in plaintext for now — will be encrypted once shared key exists
|
|
177
|
+
creatorFingerprint: identity.fingerprint,
|
|
178
|
+
creatorPublicKey: identity.publicKey,
|
|
179
|
+
creatorDisplayName: identity.displayName,
|
|
180
|
+
ttlHours: 0, // permanent until deleted
|
|
181
|
+
maxMessages: 5000,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (result.error) { fail(result.error); return; }
|
|
185
|
+
|
|
186
|
+
// Save channel locally
|
|
187
|
+
const channels = loadChannels();
|
|
188
|
+
channels.forEach(c => c.active = false);
|
|
189
|
+
channels.push({
|
|
190
|
+
id: result.id,
|
|
191
|
+
name,
|
|
192
|
+
createdAt: result.createdAt,
|
|
193
|
+
active: true,
|
|
194
|
+
role: 'creator',
|
|
195
|
+
});
|
|
196
|
+
saveChannels(channels);
|
|
197
|
+
|
|
198
|
+
console.log('');
|
|
199
|
+
ok(`Channel created: ${name}`);
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(` ${BOLD}Invite Code:${NC} ${G}${result.id}${NC}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(` ${D}Share this code securely with collaborators.${NC}`);
|
|
204
|
+
console.log(` ${D}They join with: ${C}nha collab join ${result.id}${NC}`);
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function cmdJoin(args) {
|
|
209
|
+
const inviteCode = args[0];
|
|
210
|
+
if (!inviteCode) { fail('Usage: nha collab join <invite-code>'); return; }
|
|
211
|
+
|
|
212
|
+
const identity = getOrCreateIdentity();
|
|
213
|
+
|
|
214
|
+
// Check if already joined
|
|
215
|
+
const channels = loadChannels();
|
|
216
|
+
if (channels.find(c => c.id === inviteCode)) {
|
|
217
|
+
setActiveChannel(inviteCode);
|
|
218
|
+
ok(`Already a member. Switched to this channel.`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = await apiPost(`/channels/${inviteCode}/join`, {
|
|
223
|
+
fingerprint: identity.fingerprint,
|
|
224
|
+
publicKey: identity.publicKey,
|
|
225
|
+
displayName: identity.displayName,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (result.error) { fail(result.error); return; }
|
|
229
|
+
|
|
230
|
+
// Get channel info
|
|
231
|
+
const chInfo = await apiGet(`/channels/${inviteCode}`);
|
|
232
|
+
|
|
233
|
+
channels.forEach(c => c.active = false);
|
|
234
|
+
channels.push({
|
|
235
|
+
id: inviteCode,
|
|
236
|
+
name: chInfo.name || 'Unknown',
|
|
237
|
+
createdAt: chInfo.createdAt,
|
|
238
|
+
active: true,
|
|
239
|
+
role: 'member',
|
|
240
|
+
});
|
|
241
|
+
saveChannels(channels);
|
|
242
|
+
|
|
243
|
+
ok(`Joined channel: ${chInfo.name || inviteCode} (${result.members} members)`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function cmdSend(args) {
|
|
247
|
+
const message = args.join(' ');
|
|
248
|
+
if (!message) { fail('Usage: nha collab send "your message"'); return; }
|
|
249
|
+
|
|
250
|
+
const identity = getOrCreateIdentity();
|
|
251
|
+
const channel = getActiveChannel();
|
|
252
|
+
if (!channel) { fail('No active channel. Create or join one first.'); return; }
|
|
253
|
+
|
|
254
|
+
// Get channel members for key exchange
|
|
255
|
+
const chInfo = await apiGet(`/channels/${channel.id}`);
|
|
256
|
+
if (chInfo.error) { fail(chInfo.error); return; }
|
|
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);
|
|
273
|
+
|
|
274
|
+
const result = await apiPost(`/channels/${channel.id}/messages`, {
|
|
275
|
+
senderFingerprint: identity.fingerprint,
|
|
276
|
+
nonce,
|
|
277
|
+
ciphertext,
|
|
278
|
+
type: 'text',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (result.error) { fail(result.error); return; }
|
|
282
|
+
ok(`Sent (encrypted) at ${result.timestamp}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function cmdRead(args) {
|
|
286
|
+
const identity = getOrCreateIdentity();
|
|
287
|
+
const channel = getActiveChannel();
|
|
288
|
+
if (!channel) { fail('No active channel. Create or join one first.'); return; }
|
|
289
|
+
|
|
290
|
+
const since = args[0] || ''; // optional timestamp
|
|
291
|
+
const query = since ? `?since=${since}&fp=${identity.fingerprint}` : `?fp=${identity.fingerprint}`;
|
|
292
|
+
const result = await apiGet(`/channels/${channel.id}/messages${query}`);
|
|
293
|
+
if (result.error) { fail(result.error); return; }
|
|
294
|
+
|
|
295
|
+
if (result.messages.length === 0) {
|
|
296
|
+
info('No messages yet in this channel.');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
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
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`\n ${BOLD}${channel.name}${NC} ${D}(${result.messages.length} messages)${NC}\n`);
|
|
312
|
+
|
|
313
|
+
for (const msg of result.messages) {
|
|
314
|
+
const time = new Date(msg.timestamp).toLocaleTimeString();
|
|
315
|
+
const sender = result.members?.find(m => m.fingerprint === msg.senderFingerprint);
|
|
316
|
+
const senderName = sender?.displayName || msg.senderFingerprint.slice(0, 8);
|
|
317
|
+
|
|
318
|
+
if (msg.type === 'system') {
|
|
319
|
+
console.log(` ${D}${time} — ${senderName} joined${NC}`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const plaintext = decrypt(msg.ciphertext, msg.nonce, sharedKey);
|
|
325
|
+
const isMe = msg.senderFingerprint === identity.fingerprint;
|
|
326
|
+
const color = isMe ? G : C;
|
|
327
|
+
console.log(` ${D}${time}${NC} ${color}${senderName}${NC}: ${plaintext}`);
|
|
328
|
+
} catch {
|
|
329
|
+
console.log(` ${D}${time}${NC} ${R}${senderName}${NC}: ${D}[cannot decrypt — key mismatch]${NC}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
console.log('');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function cmdList() {
|
|
336
|
+
const channels = loadChannels();
|
|
337
|
+
if (channels.length === 0) {
|
|
338
|
+
info('No channels. Create one: nha collab create "Project Name"');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`\n ${BOLD}Your Channels (${channels.length})${NC}\n`);
|
|
343
|
+
for (const ch of channels) {
|
|
344
|
+
const active = ch.active ? `${G}●${NC}` : `${D}○${NC}`;
|
|
345
|
+
console.log(` ${active} ${ch.name} ${D}(${ch.role}, ${ch.id.slice(0, 8)}...)${NC}`);
|
|
346
|
+
}
|
|
347
|
+
console.log(`\n ${D}Switch: nha collab switch <number>${NC}\n`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function cmdMembers() {
|
|
351
|
+
const channel = getActiveChannel();
|
|
352
|
+
if (!channel) { fail('No active channel.'); return; }
|
|
353
|
+
|
|
354
|
+
const chInfo = await apiGet(`/channels/${channel.id}`);
|
|
355
|
+
if (chInfo.error) { fail(chInfo.error); return; }
|
|
356
|
+
|
|
357
|
+
console.log(`\n ${BOLD}${channel.name}${NC} — ${chInfo.memberCount} members\n`);
|
|
358
|
+
for (const m of chInfo.members) {
|
|
359
|
+
const online = (Date.now() - new Date(m.lastSeen).getTime()) < 300_000;
|
|
360
|
+
const status = online ? `${G}online${NC}` : `${D}${new Date(m.lastSeen).toLocaleString()}${NC}`;
|
|
361
|
+
console.log(` ${m.displayName || m.fingerprint.slice(0, 8)} — ${status}`);
|
|
362
|
+
}
|
|
363
|
+
console.log('');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function cmdSwitch(args) {
|
|
367
|
+
const channels = loadChannels();
|
|
368
|
+
const idx = parseInt(args[0]) - 1;
|
|
369
|
+
if (isNaN(idx) || idx < 0 || idx >= channels.length) {
|
|
370
|
+
fail('Invalid channel number. Use: nha collab list');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
channels.forEach(c => c.active = false);
|
|
374
|
+
channels[idx].active = true;
|
|
375
|
+
saveChannels(channels);
|
|
376
|
+
ok(`Switched to: ${channels[idx].name}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function cmdDelete() {
|
|
380
|
+
const identity = getOrCreateIdentity();
|
|
381
|
+
const channel = getActiveChannel();
|
|
382
|
+
if (!channel) { fail('No active channel.'); return; }
|
|
383
|
+
|
|
384
|
+
const result = await apiDelete(`/channels/${channel.id}`, {
|
|
385
|
+
fingerprint: identity.fingerprint,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (result.error) { fail(result.error); return; }
|
|
389
|
+
|
|
390
|
+
const channels = loadChannels().filter(c => c.id !== channel.id);
|
|
391
|
+
if (channels.length > 0) channels[0].active = true;
|
|
392
|
+
saveChannels(channels);
|
|
393
|
+
ok(`Channel deleted: ${channel.name}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function cmdIdentity() {
|
|
397
|
+
const identity = getOrCreateIdentity();
|
|
398
|
+
console.log(`\n ${BOLD}Your Identity${NC}\n`);
|
|
399
|
+
console.log(` Fingerprint: ${G}${identity.fingerprint}${NC}`);
|
|
400
|
+
console.log(` Display Name: ${identity.displayName}`);
|
|
401
|
+
console.log(` Created: ${identity.createdAt}`);
|
|
402
|
+
console.log(` Public Key: ${D}${identity.publicKey.slice(0, 40)}...${NC}`);
|
|
403
|
+
console.log(`\n ${D}Keys stored in: ${IDENTITY_FILE}${NC}\n`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Main Router ───────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
export async function cmdCollab(args) {
|
|
409
|
+
const sub = args[0] || 'help';
|
|
410
|
+
const subArgs = args.slice(1);
|
|
411
|
+
|
|
412
|
+
switch (sub) {
|
|
413
|
+
case 'create': return cmdCreate(subArgs);
|
|
414
|
+
case 'join': return cmdJoin(subArgs);
|
|
415
|
+
case 'send': return cmdSend(subArgs);
|
|
416
|
+
case 'read': return cmdRead(subArgs);
|
|
417
|
+
case 'list': case 'ls': return cmdList();
|
|
418
|
+
case 'members': return cmdMembers();
|
|
419
|
+
case 'switch': return cmdSwitch(subArgs);
|
|
420
|
+
case 'delete': return cmdDelete();
|
|
421
|
+
case 'identity': case 'id': return cmdIdentity();
|
|
422
|
+
case 'help': default:
|
|
423
|
+
console.log(`\n ${BOLD}${Y}Alexandria${NC} — E2E Encrypted Communication\n`);
|
|
424
|
+
console.log(` ${C}nha collab create "name"${NC} Create encrypted channel`);
|
|
425
|
+
console.log(` ${C}nha collab join <code>${NC} Join with invite code`);
|
|
426
|
+
console.log(` ${C}nha collab send "msg"${NC} Encrypt and send`);
|
|
427
|
+
console.log(` ${C}nha collab read${NC} Decrypt and show messages`);
|
|
428
|
+
console.log(` ${C}nha collab list${NC} Your channels`);
|
|
429
|
+
console.log(` ${C}nha collab members${NC} Show channel members`);
|
|
430
|
+
console.log(` ${C}nha collab switch <n>${NC} Switch active channel`);
|
|
431
|
+
console.log(` ${C}nha collab delete${NC} Delete channel`);
|
|
432
|
+
console.log(` ${C}nha collab identity${NC} Show your keypair info`);
|
|
433
|
+
console.log(`\n ${D}All messages E2E encrypted (X25519 + AES-256-GCM).${NC}`);
|
|
434
|
+
console.log(` ${D}The server sees only ciphertext. No accounts needed.${NC}\n`);
|
|
435
|
+
}
|
|
436
|
+
}
|
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
|
+
export const VERSION = '10.0.0';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -938,11 +938,19 @@ async function daemonLoop() {
|
|
|
938
938
|
},
|
|
939
939
|
});
|
|
940
940
|
|
|
941
|
-
// SABER quick scan for suspicious
|
|
942
|
-
|
|
941
|
+
// SABER quick scan — only for emails with suspicious URLs (not known domains)
|
|
942
|
+
const suspiciousUrls = (email.urls || []).filter(u => {
|
|
943
|
+
try {
|
|
944
|
+
const host = new URL(u).hostname.toLowerCase();
|
|
945
|
+
// Skip known legitimate domains
|
|
946
|
+
const safe = ['google.com','accounts.google.com','paypal.com','paypal.it','apple.com','microsoft.com','github.com','linkedin.com','amazon.com','facebook.com','instagram.com','twitter.com','x.com','youtube.com','netflix.com','disneyplus.com','spotify.com'];
|
|
947
|
+
return !safe.some(d => host === d || host.endsWith('.' + d));
|
|
948
|
+
} catch { return true; } // malformed URL = suspicious
|
|
949
|
+
});
|
|
950
|
+
if (suspiciousUrls.length > 0) {
|
|
943
951
|
try {
|
|
944
952
|
const scanResult = await callAgent(config, 'saber',
|
|
945
|
-
`Quick
|
|
953
|
+
`Quick phishing scan. ONLY flag if there are CLEAR red flags (domain spoofing, credential harvesting URLs, mismatched sender/domain). Do NOT flag legitimate emails from real companies.\n\nFrom: "${email.from}"\nSubject: "${email.subject}"\nSuspicious URLs: ${suspiciousUrls.join(', ')}\n\nRespond ONLY: SAFE or FLAGGED with one-line reason. Default to SAFE unless clearly malicious.`
|
|
946
954
|
);
|
|
947
955
|
if (scanResult.toUpperCase().includes('FLAGGED')) {
|
|
948
956
|
await notify('Security Alert', `Suspicious email from ${email.from}: ${email.subject}\n${scanResult}`, config);
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -1928,7 +1928,7 @@ var wsMaxRetries = 1;
|
|
|
1928
1928
|
function connectWebSocket() {
|
|
1929
1929
|
if (wsRetryCount >= wsMaxRetries) return; // Stop trying after 3 failures
|
|
1930
1930
|
try {
|
|
1931
|
-
ws = new WebSocket('ws://' + window.location.
|
|
1931
|
+
ws = new WebSocket('ws://' + window.location.host);
|
|
1932
1932
|
} catch(e) { return; }
|
|
1933
1933
|
|
|
1934
1934
|
ws.onopen = function() {
|