myceliumail 1.0.2 → 1.0.3
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/.context7 +87 -0
- package/.eslintrc.json +29 -0
- package/COMPLETE.md +51 -0
- package/MYCELIUMAIL_STARTER_KIT.md +603 -0
- package/NEXT_STEPS.md +96 -0
- package/desktop/README.md +102 -0
- package/desktop/assets/icon.icns +0 -0
- package/desktop/assets/icon.iconset/icon_128x128.png +0 -0
- package/desktop/assets/icon.iconset/icon_128x128@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512@2x.png +0 -0
- package/desktop/assets/icon.png +0 -0
- package/desktop/assets/tray-icon.png +0 -0
- package/desktop/main.js +257 -0
- package/desktop/package-lock.json +4198 -0
- package/desktop/package.json +48 -0
- package/desktop/preload.js +11 -0
- package/dist/bin/myceliumail.js +2 -0
- package/dist/bin/myceliumail.js.map +1 -1
- package/dist/commands/key-announce.d.ts +6 -0
- package/dist/commands/key-announce.d.ts.map +1 -0
- package/dist/commands/key-announce.js +63 -0
- package/dist/commands/key-announce.js.map +1 -0
- package/docs/20251215_Treebird-Ecosystem_Knowledge-Base_v2.md +292 -0
- package/docs/20251215_Treebird-Ecosystem_Project-Instructions_v2.md +176 -0
- package/docs/AGENT_DELEGATION_WORKFLOW.md +453 -0
- package/docs/AGENT_STARTER_KIT.md +145 -0
- package/docs/ANNOUNCEMENT_DRAFTS.md +55 -0
- package/docs/DASHBOARD_AGENT_HANDOFF.md +429 -0
- package/docs/DASHBOARD_AGENT_PROMPT.md +32 -0
- package/docs/DASHBOARD_BUILD_ROADMAP.md +61 -0
- package/docs/DEPLOYMENT.md +59 -0
- package/docs/LESSONS_LEARNED.md +127 -0
- package/docs/MCP_PUBLISHING_ROADMAP.md +113 -0
- package/docs/MCP_STARTER_KIT.md +117 -0
- package/docs/SSAN_MESSAGES_SUMMARY.md +92 -0
- package/docs/STORAGE_ARCHITECTURE.md +114 -0
- package/mcp-server/README.md +143 -0
- package/mcp-server/assets/icon.png +0 -0
- package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
- package/mcp-server/package-lock.json +1142 -0
- package/mcp-server/package.json +49 -0
- package/mcp-server/src/lib/config.ts +55 -0
- package/mcp-server/src/lib/crypto.ts +150 -0
- package/mcp-server/src/lib/storage.ts +267 -0
- package/mcp-server/src/server.ts +387 -0
- package/mcp-server/tsconfig.json +26 -0
- package/package.json +3 -3
- package/src/bin/myceliumail.ts +54 -0
- package/src/commands/broadcast.ts +70 -0
- package/src/commands/dashboard.ts +19 -0
- package/src/commands/inbox.ts +75 -0
- package/src/commands/key-announce.ts +70 -0
- package/src/commands/key-import.ts +35 -0
- package/src/commands/keygen.ts +44 -0
- package/src/commands/keys.ts +55 -0
- package/src/commands/read.ts +97 -0
- package/src/commands/send.ts +89 -0
- package/src/commands/watch.ts +101 -0
- package/src/dashboard/public/app.js +523 -0
- package/src/dashboard/public/index.html +75 -0
- package/src/dashboard/public/styles.css +68 -0
- package/src/dashboard/routes.ts +128 -0
- package/src/dashboard/server.ts +33 -0
- package/src/lib/config.ts +104 -0
- package/src/lib/crypto.ts +210 -0
- package/src/lib/realtime.ts +109 -0
- package/src/storage/local.ts +209 -0
- package/src/storage/supabase.ts +336 -0
- package/src/types/index.ts +53 -0
- package/supabase/migrations/000_myceliumail_setup.sql +93 -0
- package/supabase/migrations/001_enable_realtime.sql +10 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { loadConfig } from '../lib/config.js';
|
|
3
|
+
import * as storage from '../storage/supabase.js';
|
|
4
|
+
import { loadKeyPair, decryptMessage, listOwnKeys } from '../lib/crypto.js';
|
|
5
|
+
import type { Message } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
// Try to decrypt a message using any available key
|
|
8
|
+
function tryDecryptWithAllKeys(msg: Message): Message {
|
|
9
|
+
if (!msg.encrypted || !msg.ciphertext || !msg.nonce || !msg.senderPublicKey) {
|
|
10
|
+
return msg;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Get all available keypairs
|
|
14
|
+
const ownKeys = listOwnKeys();
|
|
15
|
+
|
|
16
|
+
for (const agentId of ownKeys) {
|
|
17
|
+
const keyPair = loadKeyPair(agentId);
|
|
18
|
+
if (!keyPair) continue;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const decryptedText = decryptMessage({
|
|
22
|
+
ciphertext: msg.ciphertext,
|
|
23
|
+
nonce: msg.nonce,
|
|
24
|
+
senderPublicKey: msg.senderPublicKey
|
|
25
|
+
}, keyPair);
|
|
26
|
+
|
|
27
|
+
if (decryptedText) {
|
|
28
|
+
const parsed = JSON.parse(decryptedText);
|
|
29
|
+
return {
|
|
30
|
+
...msg,
|
|
31
|
+
subject: parsed.subject,
|
|
32
|
+
body: parsed.body,
|
|
33
|
+
decrypted: true,
|
|
34
|
+
decryptedBy: agentId
|
|
35
|
+
} as Message & { decrypted: boolean; decryptedBy: string };
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Try next key
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return msg;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function registerRoutes(fastify: FastifyInstance) {
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
const agentId = config.agentId;
|
|
48
|
+
|
|
49
|
+
// GET /api/inbox
|
|
50
|
+
fastify.get('/api/inbox', async (request, reply) => {
|
|
51
|
+
const messages = await storage.getInbox(agentId, { limit: 100 });
|
|
52
|
+
|
|
53
|
+
// Decrypt encrypted messages using all available keys
|
|
54
|
+
const decrypted = messages.map(tryDecryptWithAllKeys);
|
|
55
|
+
|
|
56
|
+
return { messages: decrypted, total: decrypted.length };
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// GET /api/message/:id
|
|
60
|
+
fastify.get('/api/message/:id', async (request, reply) => {
|
|
61
|
+
const { id } = request.params as { id: string };
|
|
62
|
+
const message = await storage.getMessage(id);
|
|
63
|
+
|
|
64
|
+
if (!message) {
|
|
65
|
+
return reply.code(404).send({ error: 'Message not found' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try all keys for decryption
|
|
69
|
+
return tryDecryptWithAllKeys(message);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// POST /api/message/:id/read
|
|
73
|
+
fastify.post('/api/message/:id/read', async (request, reply) => {
|
|
74
|
+
const { id } = request.params as { id: string };
|
|
75
|
+
const { readerId } = request.body as { readerId?: string };
|
|
76
|
+
await storage.markAsRead(id, readerId || agentId);
|
|
77
|
+
return { success: true };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// POST /api/message/:id/archive
|
|
81
|
+
fastify.post('/api/message/:id/archive', async (request, reply) => {
|
|
82
|
+
const { id } = request.params as { id: string };
|
|
83
|
+
await storage.archiveMessage(id);
|
|
84
|
+
return { success: true };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// DELETE /api/message/:id
|
|
88
|
+
fastify.delete('/api/message/:id', async (request, reply) => {
|
|
89
|
+
const { id } = request.params as { id: string };
|
|
90
|
+
const deleted = await storage.deleteMessage(id);
|
|
91
|
+
return { success: deleted };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// POST /api/send - Send a new message (supports multi-recipient)
|
|
95
|
+
fastify.post('/api/send', async (request, reply) => {
|
|
96
|
+
const { to, subject, body, from, attachments } = request.body as {
|
|
97
|
+
to: string | string[];
|
|
98
|
+
subject: string;
|
|
99
|
+
body: string;
|
|
100
|
+
from?: string;
|
|
101
|
+
attachments?: { name: string; type: string; data: string; size: number }[];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const sender = from || agentId;
|
|
105
|
+
const message = await storage.sendMessage(sender, to, subject, body, { attachments });
|
|
106
|
+
return { success: true, message };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// GET /api/stats
|
|
110
|
+
fastify.get('/api/stats', async (request, reply) => {
|
|
111
|
+
const messages = await storage.getInbox(agentId);
|
|
112
|
+
const unread = messages.filter(m => !m.readBy?.includes(agentId) && !m.read).length;
|
|
113
|
+
return {
|
|
114
|
+
total: messages.length,
|
|
115
|
+
unread,
|
|
116
|
+
encrypted: messages.filter(m => m.encrypted).length
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// GET /api/config - Provide config for frontend Realtime
|
|
121
|
+
fastify.get('/api/config', async (request, reply) => {
|
|
122
|
+
return {
|
|
123
|
+
agentId: config.agentId,
|
|
124
|
+
supabaseUrl: config.supabaseUrl,
|
|
125
|
+
supabaseKey: config.supabaseKey
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import fastifyStatic from '@fastify/static';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export async function startDashboard(port = 3737) {
|
|
10
|
+
const fastify = Fastify({ logger: true });
|
|
11
|
+
|
|
12
|
+
// Serve static files
|
|
13
|
+
await fastify.register(fastifyStatic, {
|
|
14
|
+
root: join(__dirname, 'public'),
|
|
15
|
+
prefix: '/'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Register routes
|
|
19
|
+
// dynamic import to avoid circular dependencies if any, though here it is clean
|
|
20
|
+
const { registerRoutes } = await import('./routes.js');
|
|
21
|
+
await registerRoutes(fastify);
|
|
22
|
+
|
|
23
|
+
// Start server
|
|
24
|
+
try {
|
|
25
|
+
await fastify.listen({ port, host: '127.0.0.1' });
|
|
26
|
+
console.log(`🍄 Dashboard running on http://localhost:${port}`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
fastify.log.error(err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fastify;
|
|
33
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail Configuration
|
|
3
|
+
*
|
|
4
|
+
* Handles loading agent configuration from environment or config file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.myceliumail');
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
export interface Config {
|
|
15
|
+
agentId: string;
|
|
16
|
+
supabaseUrl?: string;
|
|
17
|
+
supabaseKey?: string;
|
|
18
|
+
storageMode: 'auto' | 'supabase' | 'local';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensure config directory exists
|
|
23
|
+
*/
|
|
24
|
+
function ensureConfigDir(): void {
|
|
25
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load configuration from file or environment
|
|
32
|
+
*/
|
|
33
|
+
export function loadConfig(): Config {
|
|
34
|
+
// Environment variables take precedence
|
|
35
|
+
const envAgentId = process.env.MYCELIUMAIL_AGENT_ID || process.env.MYCELIUMAIL_AGENT;
|
|
36
|
+
const envSupabaseUrl = process.env.SUPABASE_URL;
|
|
37
|
+
const envSupabaseKey = process.env.SUPABASE_ANON_KEY;
|
|
38
|
+
const envStorageMode = process.env.MYCELIUMAIL_STORAGE as 'auto' | 'supabase' | 'local' | undefined;
|
|
39
|
+
|
|
40
|
+
// Try to load from config file
|
|
41
|
+
let fileConfig: Partial<Config> = {};
|
|
42
|
+
if (existsSync(CONFIG_FILE)) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
fileConfig = {
|
|
47
|
+
agentId: parsed.agent_id,
|
|
48
|
+
supabaseUrl: parsed.supabase_url,
|
|
49
|
+
supabaseKey: parsed.supabase_key,
|
|
50
|
+
storageMode: parsed.storage_mode,
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
// Invalid config file, ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Merge with env taking precedence
|
|
58
|
+
const config: Config = {
|
|
59
|
+
agentId: envAgentId || fileConfig.agentId || 'anonymous',
|
|
60
|
+
supabaseUrl: envSupabaseUrl || fileConfig.supabaseUrl,
|
|
61
|
+
supabaseKey: envSupabaseKey || fileConfig.supabaseKey,
|
|
62
|
+
storageMode: envStorageMode || fileConfig.storageMode || 'auto',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Save configuration to file
|
|
70
|
+
*/
|
|
71
|
+
export function saveConfig(config: Partial<Config>): void {
|
|
72
|
+
ensureConfigDir();
|
|
73
|
+
|
|
74
|
+
// Load existing config
|
|
75
|
+
let existing: Record<string, string> = {};
|
|
76
|
+
if (existsSync(CONFIG_FILE)) {
|
|
77
|
+
try {
|
|
78
|
+
existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
79
|
+
} catch {
|
|
80
|
+
// Start fresh
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge
|
|
85
|
+
if (config.agentId) existing.agent_id = config.agentId;
|
|
86
|
+
if (config.supabaseUrl) existing.supabase_url = config.supabaseUrl;
|
|
87
|
+
if (config.supabaseKey) existing.supabase_key = config.supabaseKey;
|
|
88
|
+
|
|
89
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get config directory path
|
|
94
|
+
*/
|
|
95
|
+
export function getConfigDir(): string {
|
|
96
|
+
return CONFIG_DIR;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if Supabase is configured
|
|
101
|
+
*/
|
|
102
|
+
export function hasSupabaseConfig(config: Config): boolean {
|
|
103
|
+
return !!(config.supabaseUrl && config.supabaseKey);
|
|
104
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail Crypto Module
|
|
3
|
+
*
|
|
4
|
+
* E2E encryption for agent messaging using TweetNaCl.
|
|
5
|
+
* Uses X25519 for key exchange and XSalsa20-Poly1305 for encryption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import nacl from 'tweetnacl';
|
|
9
|
+
import util from 'tweetnacl-util';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
// Key storage location
|
|
15
|
+
const KEYS_DIR = join(homedir(), '.myceliumail', 'keys');
|
|
16
|
+
|
|
17
|
+
export interface KeyPair {
|
|
18
|
+
publicKey: Uint8Array;
|
|
19
|
+
secretKey: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EncryptedMessage {
|
|
23
|
+
ciphertext: string; // base64
|
|
24
|
+
nonce: string; // base64
|
|
25
|
+
senderPublicKey: string; // base64
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensure keys directory exists
|
|
30
|
+
*/
|
|
31
|
+
function ensureKeysDir(): void {
|
|
32
|
+
if (!existsSync(KEYS_DIR)) {
|
|
33
|
+
mkdirSync(KEYS_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a new keypair for an agent
|
|
39
|
+
*/
|
|
40
|
+
export function generateKeyPair(): KeyPair {
|
|
41
|
+
return nacl.box.keyPair();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Save keypair to local storage
|
|
46
|
+
*/
|
|
47
|
+
export function saveKeyPair(agentId: string, keyPair: KeyPair): void {
|
|
48
|
+
ensureKeysDir();
|
|
49
|
+
const serialized = {
|
|
50
|
+
publicKey: util.encodeBase64(keyPair.publicKey),
|
|
51
|
+
secretKey: util.encodeBase64(keyPair.secretKey),
|
|
52
|
+
};
|
|
53
|
+
const path = join(KEYS_DIR, `${agentId}.key.json`);
|
|
54
|
+
writeFileSync(path, JSON.stringify(serialized, null, 2), { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load keypair from local storage
|
|
59
|
+
*/
|
|
60
|
+
export function loadKeyPair(agentId: string): KeyPair | null {
|
|
61
|
+
const path = join(KEYS_DIR, `${agentId}.key.json`);
|
|
62
|
+
if (!existsSync(path)) return null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
66
|
+
return {
|
|
67
|
+
publicKey: util.decodeBase64(data.publicKey),
|
|
68
|
+
secretKey: util.decodeBase64(data.secretKey),
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if keypair exists for an agent
|
|
77
|
+
*/
|
|
78
|
+
export function hasKeyPair(agentId: string): boolean {
|
|
79
|
+
const path = join(KEYS_DIR, `${agentId}.key.json`);
|
|
80
|
+
return existsSync(path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get public key as base64 string
|
|
85
|
+
*/
|
|
86
|
+
export function getPublicKeyBase64(keyPair: KeyPair): string {
|
|
87
|
+
return util.encodeBase64(keyPair.publicKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Encrypt a message for a recipient
|
|
92
|
+
*/
|
|
93
|
+
export function encryptMessage(
|
|
94
|
+
message: string,
|
|
95
|
+
recipientPublicKey: Uint8Array,
|
|
96
|
+
senderKeyPair: KeyPair
|
|
97
|
+
): EncryptedMessage {
|
|
98
|
+
const messageBytes = util.decodeUTF8(message);
|
|
99
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
100
|
+
|
|
101
|
+
const ciphertext = nacl.box(
|
|
102
|
+
messageBytes,
|
|
103
|
+
nonce,
|
|
104
|
+
recipientPublicKey,
|
|
105
|
+
senderKeyPair.secretKey
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ciphertext: util.encodeBase64(ciphertext),
|
|
110
|
+
nonce: util.encodeBase64(nonce),
|
|
111
|
+
senderPublicKey: util.encodeBase64(senderKeyPair.publicKey),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Decrypt a message from a sender
|
|
117
|
+
*/
|
|
118
|
+
export function decryptMessage(
|
|
119
|
+
encrypted: EncryptedMessage,
|
|
120
|
+
recipientKeyPair: KeyPair
|
|
121
|
+
): string | null {
|
|
122
|
+
try {
|
|
123
|
+
const ciphertext = util.decodeBase64(encrypted.ciphertext);
|
|
124
|
+
const nonce = util.decodeBase64(encrypted.nonce);
|
|
125
|
+
const senderPublicKey = util.decodeBase64(encrypted.senderPublicKey);
|
|
126
|
+
|
|
127
|
+
const decrypted = nacl.box.open(
|
|
128
|
+
ciphertext,
|
|
129
|
+
nonce,
|
|
130
|
+
senderPublicKey,
|
|
131
|
+
recipientKeyPair.secretKey
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!decrypted) return null;
|
|
135
|
+
return util.encodeUTF8(decrypted);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Known keys registry - maps agent IDs to their public keys
|
|
143
|
+
*/
|
|
144
|
+
export function loadKnownKeys(): Record<string, string> {
|
|
145
|
+
const path = join(KEYS_DIR, 'known_keys.json');
|
|
146
|
+
if (!existsSync(path)) return {};
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
149
|
+
} catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Save a known key for an agent
|
|
156
|
+
*/
|
|
157
|
+
export function saveKnownKey(agentId: string, publicKeyBase64: string): void {
|
|
158
|
+
ensureKeysDir();
|
|
159
|
+
const keys = loadKnownKeys();
|
|
160
|
+
keys[agentId] = publicKeyBase64;
|
|
161
|
+
writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get known key for an agent
|
|
166
|
+
*/
|
|
167
|
+
export function getKnownKey(agentId: string): string | null {
|
|
168
|
+
const keys = loadKnownKeys();
|
|
169
|
+
return keys[agentId] || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get all known keys
|
|
174
|
+
*/
|
|
175
|
+
export function getKnownKeys(): Record<string, string> {
|
|
176
|
+
return loadKnownKeys();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete a known key
|
|
181
|
+
*/
|
|
182
|
+
export function deleteKnownKey(agentId: string): boolean {
|
|
183
|
+
const keys = loadKnownKeys();
|
|
184
|
+
if (!(agentId in keys)) return false;
|
|
185
|
+
delete keys[agentId];
|
|
186
|
+
writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* List all own keypairs (for agents we have keys for)
|
|
192
|
+
*/
|
|
193
|
+
export function listOwnKeys(): string[] {
|
|
194
|
+
ensureKeysDir();
|
|
195
|
+
try {
|
|
196
|
+
const files = readdirSync(KEYS_DIR);
|
|
197
|
+
return files
|
|
198
|
+
.filter(f => f.endsWith('.key.json'))
|
|
199
|
+
.map(f => f.replace('.key.json', ''));
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Decode a base64 public key to Uint8Array
|
|
207
|
+
*/
|
|
208
|
+
export function decodePublicKey(base64: string): Uint8Array {
|
|
209
|
+
return util.decodeBase64(base64);
|
|
210
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Realtime Module
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time subscription to new messages using Supabase Realtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createClient, RealtimeChannel, SupabaseClient } from '@supabase/supabase-js';
|
|
8
|
+
import { loadConfig, hasSupabaseConfig } from './config.js';
|
|
9
|
+
|
|
10
|
+
interface RealtimeMessage {
|
|
11
|
+
id: string;
|
|
12
|
+
from_agent: string;
|
|
13
|
+
to_agent: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
message: string;
|
|
16
|
+
encrypted: boolean;
|
|
17
|
+
read: boolean;
|
|
18
|
+
created_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MessageCallback {
|
|
22
|
+
(message: RealtimeMessage): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatusCallback {
|
|
26
|
+
(status: 'SUBSCRIBED' | 'CLOSED' | 'CHANNEL_ERROR' | 'TIMED_OUT', error?: Error): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let supabaseClient: SupabaseClient | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get or create the Supabase client for Realtime
|
|
33
|
+
*/
|
|
34
|
+
function getClient(): SupabaseClient | null {
|
|
35
|
+
if (supabaseClient) return supabaseClient;
|
|
36
|
+
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
|
|
39
|
+
if (!hasSupabaseConfig(config)) {
|
|
40
|
+
console.error('❌ Supabase not configured. Set SUPABASE_URL and SUPABASE_ANON_KEY.');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
supabaseClient = createClient(config.supabaseUrl!, config.supabaseKey!, {
|
|
45
|
+
realtime: {
|
|
46
|
+
params: {
|
|
47
|
+
eventsPerSecond: 10,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return supabaseClient;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to new messages for a specific agent
|
|
57
|
+
*/
|
|
58
|
+
export function subscribeToMessages(
|
|
59
|
+
agentId: string,
|
|
60
|
+
onMessage: MessageCallback,
|
|
61
|
+
onStatus?: StatusCallback
|
|
62
|
+
): RealtimeChannel | null {
|
|
63
|
+
const client = getClient();
|
|
64
|
+
if (!client) return null;
|
|
65
|
+
|
|
66
|
+
const channel = client
|
|
67
|
+
.channel('inbox-watch')
|
|
68
|
+
.on<RealtimeMessage>(
|
|
69
|
+
'postgres_changes',
|
|
70
|
+
{
|
|
71
|
+
event: 'INSERT',
|
|
72
|
+
schema: 'public',
|
|
73
|
+
table: 'agent_messages',
|
|
74
|
+
filter: `to_agent=eq.${agentId}`,
|
|
75
|
+
},
|
|
76
|
+
(payload) => {
|
|
77
|
+
onMessage(payload.new);
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
.subscribe((status, err) => {
|
|
81
|
+
if (onStatus) {
|
|
82
|
+
// Only pass error if it exists and has meaningful content
|
|
83
|
+
const error = err && (err.message || err.toString()) ? err : undefined;
|
|
84
|
+
onStatus(status as 'SUBSCRIBED' | 'CLOSED' | 'CHANNEL_ERROR' | 'TIMED_OUT', error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return channel;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unsubscribe from a channel
|
|
93
|
+
*/
|
|
94
|
+
export async function unsubscribe(channel: RealtimeChannel): Promise<void> {
|
|
95
|
+
const client = getClient();
|
|
96
|
+
if (client && channel) {
|
|
97
|
+
await client.removeChannel(channel);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Close all realtime connections
|
|
103
|
+
*/
|
|
104
|
+
export async function closeConnection(): Promise<void> {
|
|
105
|
+
if (supabaseClient) {
|
|
106
|
+
await supabaseClient.removeAllChannels();
|
|
107
|
+
supabaseClient = null;
|
|
108
|
+
}
|
|
109
|
+
}
|