@agenticmail/enterprise 0.2.1

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.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Admin API Routes
3
+ *
4
+ * CRUD for agents, users, audit logs, rules, settings.
5
+ * All routes are protected by auth middleware (applied in server.ts).
6
+ * Input validation on all mutations. RBAC on sensitive operations.
7
+ */
8
+
9
+ import { Hono } from 'hono';
10
+ import type { DatabaseAdapter } from '../db/adapter.js';
11
+ import { validate, requireRole, ValidationError } from '../middleware/index.js';
12
+
13
+ export function createAdminRoutes(db: DatabaseAdapter) {
14
+ const api = new Hono();
15
+
16
+ // ─── Dashboard Stats ────────────────────────────────
17
+
18
+ api.get('/stats', async (c) => {
19
+ const stats = await db.getStats();
20
+ return c.json(stats);
21
+ });
22
+
23
+ // ─── Agents ─────────────────────────────────────────
24
+
25
+ api.get('/agents', async (c) => {
26
+ const status = c.req.query('status') as any;
27
+ const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
28
+ const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
29
+ const agents = await db.listAgents({ status, limit, offset });
30
+ const total = await db.countAgents(status);
31
+ return c.json({ agents, total, limit, offset });
32
+ });
33
+
34
+ api.get('/agents/:id', async (c) => {
35
+ const agent = await db.getAgent(c.req.param('id'));
36
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
37
+ return c.json(agent);
38
+ });
39
+
40
+ api.post('/agents', async (c) => {
41
+ const body = await c.req.json();
42
+ validate(body, [
43
+ { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },
44
+ { field: 'email', type: 'email' },
45
+ { field: 'role', type: 'string', maxLength: 32 },
46
+ ]);
47
+
48
+ // Check for duplicate name
49
+ const existing = await db.getAgentByName(body.name);
50
+ if (existing) {
51
+ return c.json({ error: 'Agent name already exists' }, 409);
52
+ }
53
+
54
+ const userId = c.get('userId' as any) || 'system';
55
+ const agent = await db.createAgent({ ...body, createdBy: userId });
56
+ return c.json(agent, 201);
57
+ });
58
+
59
+ api.patch('/agents/:id', async (c) => {
60
+ const id = c.req.param('id');
61
+ const existing = await db.getAgent(id);
62
+ if (!existing) return c.json({ error: 'Agent not found' }, 404);
63
+
64
+ const body = await c.req.json();
65
+ validate(body, [
66
+ { field: 'name', type: 'string', minLength: 1, maxLength: 64 },
67
+ { field: 'email', type: 'email' },
68
+ { field: 'role', type: 'string', maxLength: 32 },
69
+ { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },
70
+ ]);
71
+
72
+ // If renaming, check for conflicts
73
+ if (body.name && body.name !== existing.name) {
74
+ const conflict = await db.getAgentByName(body.name);
75
+ if (conflict) return c.json({ error: 'Agent name already exists' }, 409);
76
+ }
77
+
78
+ const agent = await db.updateAgent(id, body);
79
+ return c.json(agent);
80
+ });
81
+
82
+ api.post('/agents/:id/archive', async (c) => {
83
+ const existing = await db.getAgent(c.req.param('id'));
84
+ if (!existing) return c.json({ error: 'Agent not found' }, 404);
85
+ if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);
86
+
87
+ await db.archiveAgent(c.req.param('id'));
88
+ return c.json({ ok: true, status: 'archived' });
89
+ });
90
+
91
+ api.post('/agents/:id/restore', async (c) => {
92
+ const existing = await db.getAgent(c.req.param('id'));
93
+ if (!existing) return c.json({ error: 'Agent not found' }, 404);
94
+ if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);
95
+
96
+ await db.updateAgent(c.req.param('id'), { status: 'active' } as any);
97
+ return c.json({ ok: true, status: 'active' });
98
+ });
99
+
100
+ // Permanent delete — owner/admin only
101
+ api.delete('/agents/:id', requireRole('admin'), async (c) => {
102
+ const existing = await db.getAgent(c.req.param('id'));
103
+ if (!existing) return c.json({ error: 'Agent not found' }, 404);
104
+
105
+ await db.deleteAgent(c.req.param('id'));
106
+ return c.json({ ok: true });
107
+ });
108
+
109
+ // ─── Users ──────────────────────────────────────────
110
+
111
+ api.get('/users', requireRole('admin'), async (c) => {
112
+ const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
113
+ const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
114
+ const users = await db.listUsers({ limit, offset });
115
+ // Strip password hashes
116
+ const safe = users.map(({ passwordHash, ...u }) => u);
117
+ return c.json({ users: safe, limit, offset });
118
+ });
119
+
120
+ api.post('/users', requireRole('admin'), async (c) => {
121
+ const body = await c.req.json();
122
+ validate(body, [
123
+ { field: 'email', type: 'email', required: true },
124
+ { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
125
+ { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },
126
+ { field: 'password', type: 'string', minLength: 8, maxLength: 128 },
127
+ ]);
128
+
129
+ // Check duplicate email
130
+ const existing = await db.getUserByEmail(body.email);
131
+ if (existing) return c.json({ error: 'Email already registered' }, 409);
132
+
133
+ const user = await db.createUser(body);
134
+ const { passwordHash, ...safe } = user;
135
+ return c.json(safe, 201);
136
+ });
137
+
138
+ api.patch('/users/:id', requireRole('admin'), async (c) => {
139
+ const existing = await db.getUser(c.req.param('id'));
140
+ if (!existing) return c.json({ error: 'User not found' }, 404);
141
+
142
+ const body = await c.req.json();
143
+ validate(body, [
144
+ { field: 'email', type: 'email' },
145
+ { field: 'name', type: 'string', minLength: 1, maxLength: 128 },
146
+ { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },
147
+ ]);
148
+
149
+ const user = await db.updateUser(c.req.param('id'), body);
150
+ const { passwordHash, ...safe } = user;
151
+ return c.json(safe);
152
+ });
153
+
154
+ api.delete('/users/:id', requireRole('owner'), async (c) => {
155
+ const existing = await db.getUser(c.req.param('id'));
156
+ if (!existing) return c.json({ error: 'User not found' }, 404);
157
+
158
+ // Cannot delete yourself
159
+ const requesterId = c.get('userId' as any);
160
+ if (requesterId === c.req.param('id')) {
161
+ return c.json({ error: 'Cannot delete your own account' }, 400);
162
+ }
163
+
164
+ await db.deleteUser(c.req.param('id'));
165
+ return c.json({ ok: true });
166
+ });
167
+
168
+ // ─── Audit Log ──────────────────────────────────────
169
+
170
+ api.get('/audit', requireRole('admin'), async (c) => {
171
+ const filters = {
172
+ actor: c.req.query('actor') || undefined,
173
+ action: c.req.query('action') || undefined,
174
+ resource: c.req.query('resource') || undefined,
175
+ from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,
176
+ to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,
177
+ limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),
178
+ offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),
179
+ };
180
+
181
+ // Validate date params
182
+ if (filters.from && isNaN(filters.from.getTime())) {
183
+ return c.json({ error: 'Invalid "from" date' }, 400);
184
+ }
185
+ if (filters.to && isNaN(filters.to.getTime())) {
186
+ return c.json({ error: 'Invalid "to" date' }, 400);
187
+ }
188
+
189
+ const result = await db.queryAudit(filters);
190
+ return c.json(result);
191
+ });
192
+
193
+ // ─── API Keys ───────────────────────────────────────
194
+
195
+ api.get('/api-keys', requireRole('admin'), async (c) => {
196
+ const keys = await db.listApiKeys();
197
+ // Never expose key hashes
198
+ const safe = keys.map(({ keyHash, ...k }) => k);
199
+ return c.json({ keys: safe });
200
+ });
201
+
202
+ api.post('/api-keys', requireRole('admin'), async (c) => {
203
+ const body = await c.req.json();
204
+ validate(body, [
205
+ { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },
206
+ ]);
207
+
208
+ const userId = c.get('userId' as any) || 'system';
209
+ const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];
210
+ const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;
211
+
212
+ const { key, plaintext } = await db.createApiKey({
213
+ name: body.name,
214
+ scopes,
215
+ createdBy: userId,
216
+ expiresAt,
217
+ });
218
+
219
+ // Only time the plaintext key is returned — emphasize this
220
+ const { keyHash, ...safeKey } = key;
221
+ return c.json({
222
+ key: safeKey,
223
+ plaintext,
224
+ warning: 'Store this key securely. It will not be shown again.',
225
+ }, 201);
226
+ });
227
+
228
+ api.delete('/api-keys/:id', requireRole('admin'), async (c) => {
229
+ const existing = await db.getApiKey(c.req.param('id'));
230
+ if (!existing) return c.json({ error: 'API key not found' }, 404);
231
+
232
+ await db.revokeApiKey(c.req.param('id'));
233
+ return c.json({ ok: true, revoked: true });
234
+ });
235
+
236
+ // ─── Email Rules ────────────────────────────────────
237
+
238
+ api.get('/rules', async (c) => {
239
+ const agentId = c.req.query('agentId') || undefined;
240
+ const rules = await db.getRules(agentId);
241
+ return c.json({ rules });
242
+ });
243
+
244
+ api.post('/rules', async (c) => {
245
+ const body = await c.req.json();
246
+ validate(body, [
247
+ { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
248
+ ]);
249
+
250
+ // Validate conditions/actions are objects
251
+ if (body.conditions && typeof body.conditions !== 'object') {
252
+ return c.json({ error: 'conditions must be an object' }, 400);
253
+ }
254
+ if (body.actions && typeof body.actions !== 'object') {
255
+ return c.json({ error: 'actions must be an object' }, 400);
256
+ }
257
+
258
+ const rule = await db.createRule({
259
+ name: body.name,
260
+ agentId: body.agentId,
261
+ conditions: body.conditions || {},
262
+ actions: body.actions || {},
263
+ priority: body.priority ?? 0,
264
+ enabled: body.enabled ?? true,
265
+ });
266
+ return c.json(rule, 201);
267
+ });
268
+
269
+ api.patch('/rules/:id', async (c) => {
270
+ const body = await c.req.json();
271
+ const rule = await db.updateRule(c.req.param('id'), body);
272
+ return c.json(rule);
273
+ });
274
+
275
+ api.delete('/rules/:id', async (c) => {
276
+ await db.deleteRule(c.req.param('id'));
277
+ return c.json({ ok: true });
278
+ });
279
+
280
+ // ─── Settings ───────────────────────────────────────
281
+
282
+ api.get('/settings', async (c) => {
283
+ const settings = await db.getSettings();
284
+ if (!settings) return c.json({ error: 'Not configured' }, 404);
285
+
286
+ // Redact sensitive fields
287
+ const safe = { ...settings };
288
+ if (safe.smtpPass) safe.smtpPass = '***';
289
+ if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';
290
+ return c.json(safe);
291
+ });
292
+
293
+ api.patch('/settings', requireRole('admin'), async (c) => {
294
+ const body = await c.req.json();
295
+ validate(body, [
296
+ { field: 'name', type: 'string', minLength: 1, maxLength: 128 },
297
+ { field: 'domain', type: 'string', maxLength: 253 },
298
+ { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },
299
+ { field: 'logoUrl', type: 'url' },
300
+ ]);
301
+
302
+ const settings = await db.updateSettings(body);
303
+ return c.json(settings);
304
+ });
305
+
306
+ // ─── Retention ──────────────────────────────────────
307
+
308
+ api.get('/retention', requireRole('admin'), async (c) => {
309
+ const policy = await db.getRetentionPolicy();
310
+ return c.json(policy);
311
+ });
312
+
313
+ api.put('/retention', requireRole('owner'), async (c) => {
314
+ const body = await c.req.json();
315
+ validate(body, [
316
+ { field: 'enabled', type: 'boolean', required: true },
317
+ { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },
318
+ { field: 'archiveFirst', type: 'boolean' },
319
+ ]);
320
+
321
+ await db.setRetentionPolicy({
322
+ enabled: body.enabled,
323
+ retainDays: body.retainDays,
324
+ excludeTags: body.excludeTags || [],
325
+ archiveFirst: body.archiveFirst ?? true,
326
+ });
327
+ return c.json({ ok: true });
328
+ });
329
+
330
+ return api;
331
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Authentication Routes
3
+ *
4
+ * Handles login (email/password), JWT issuance, SAML, and OIDC callbacks.
5
+ */
6
+
7
+ import { Hono } from 'hono';
8
+ import type { DatabaseAdapter } from '../db/adapter.js';
9
+
10
+ export function createAuthRoutes(db: DatabaseAdapter, jwtSecret: string) {
11
+ const auth = new Hono();
12
+
13
+ // ─── Email/Password Login ───────────────────────────────
14
+
15
+ auth.post('/login', async (c) => {
16
+ const { email, password } = await c.req.json();
17
+ if (!email || !password) {
18
+ return c.json({ error: 'Email and password required' }, 400);
19
+ }
20
+
21
+ const user = await db.getUserByEmail(email);
22
+ if (!user || !user.passwordHash) {
23
+ return c.json({ error: 'Invalid credentials' }, 401);
24
+ }
25
+
26
+ const { default: bcrypt } = await import('bcryptjs');
27
+ const valid = await bcrypt.compare(password, user.passwordHash);
28
+ if (!valid) {
29
+ return c.json({ error: 'Invalid credentials' }, 401);
30
+ }
31
+
32
+ // Issue JWT
33
+ const { SignJWT } = await import('jose');
34
+ const secret = new TextEncoder().encode(jwtSecret);
35
+ const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role })
36
+ .setProtectedHeader({ alg: 'HS256' })
37
+ .setIssuedAt()
38
+ .setExpirationTime('24h')
39
+ .sign(secret);
40
+
41
+ // Update last login
42
+ await db.updateUser(user.id, { lastLoginAt: new Date() } as any);
43
+ await db.logEvent({
44
+ actor: user.id, actorType: 'user', action: 'auth.login',
45
+ resource: `user:${user.id}`, details: { method: 'password' },
46
+ ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),
47
+ });
48
+
49
+ return c.json({
50
+ token,
51
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
52
+ });
53
+ });
54
+
55
+ // ─── Token Refresh ──────────────────────────────────────
56
+
57
+ auth.post('/refresh', async (c) => {
58
+ const authHeader = c.req.header('Authorization');
59
+ if (!authHeader?.startsWith('Bearer ')) {
60
+ return c.json({ error: 'Token required' }, 401);
61
+ }
62
+
63
+ try {
64
+ const { jwtVerify, SignJWT } = await import('jose');
65
+ const secret = new TextEncoder().encode(jwtSecret);
66
+ const { payload } = await jwtVerify(authHeader.slice(7), secret);
67
+
68
+ const user = await db.getUser(payload.sub as string);
69
+ if (!user) return c.json({ error: 'User not found' }, 401);
70
+
71
+ const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role })
72
+ .setProtectedHeader({ alg: 'HS256' })
73
+ .setIssuedAt()
74
+ .setExpirationTime('24h')
75
+ .sign(secret);
76
+
77
+ return c.json({ token });
78
+ } catch {
79
+ return c.json({ error: 'Invalid token' }, 401);
80
+ }
81
+ });
82
+
83
+ // ─── Current User ───────────────────────────────────────
84
+
85
+ auth.get('/me', async (c) => {
86
+ const authHeader = c.req.header('Authorization');
87
+ if (!authHeader?.startsWith('Bearer ')) {
88
+ return c.json({ error: 'Token required' }, 401);
89
+ }
90
+
91
+ try {
92
+ const { jwtVerify } = await import('jose');
93
+ const secret = new TextEncoder().encode(jwtSecret);
94
+ const { payload } = await jwtVerify(authHeader.slice(7), secret);
95
+ const user = await db.getUser(payload.sub as string);
96
+ if (!user) return c.json({ error: 'User not found' }, 404);
97
+ const { passwordHash, ...safe } = user;
98
+ return c.json(safe);
99
+ } catch {
100
+ return c.json({ error: 'Invalid token' }, 401);
101
+ }
102
+ });
103
+
104
+ // ─── SAML 2.0 (Placeholder) ────────────────────────────
105
+
106
+ auth.post('/saml/callback', async (c) => {
107
+ // TODO: Implement SAML assertion parsing
108
+ // Will use saml2-js or passport-saml
109
+ return c.json({ error: 'SAML not yet configured' }, 501);
110
+ });
111
+
112
+ auth.get('/saml/metadata', async (c) => {
113
+ // TODO: Generate SP metadata XML
114
+ return c.json({ error: 'SAML not yet configured' }, 501);
115
+ });
116
+
117
+ // ─── OIDC (Placeholder) ────────────────────────────────
118
+
119
+ auth.get('/oidc/authorize', async (c) => {
120
+ // TODO: Redirect to IdP authorization endpoint
121
+ return c.json({ error: 'OIDC not yet configured' }, 501);
122
+ });
123
+
124
+ auth.get('/oidc/callback', async (c) => {
125
+ // TODO: Handle OIDC callback, exchange code for tokens
126
+ return c.json({ error: 'OIDC not yet configured' }, 501);
127
+ });
128
+
129
+ return auth;
130
+ }