@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
package/src/server.ts ADDED
@@ -0,0 +1,310 @@
1
+ /**
2
+ * AgenticMail Enterprise Server
3
+ *
4
+ * Hono-based API server with full middleware stack.
5
+ * Production-ready: rate limiting, audit logging, RBAC,
6
+ * health checks, graceful shutdown.
7
+ */
8
+
9
+ import { Hono } from 'hono';
10
+ import { cors } from 'hono/cors';
11
+ import { serve } from '@hono/node-server';
12
+ import { readFileSync } from 'fs';
13
+ import { fileURLToPath } from 'url';
14
+ import { dirname, join } from 'path';
15
+ import type { DatabaseAdapter } from './db/adapter.js';
16
+ import { createAdminRoutes } from './admin/routes.js';
17
+ import { createAuthRoutes } from './auth/routes.js';
18
+ import {
19
+ requestIdMiddleware,
20
+ requestLogger,
21
+ rateLimiter,
22
+ securityHeaders,
23
+ errorHandler,
24
+ auditLogger,
25
+ requireRole,
26
+ } from './middleware/index.js';
27
+ import { HealthMonitor, CircuitBreaker } from './lib/resilience.js';
28
+
29
+ export interface ServerConfig {
30
+ port: number;
31
+ db: DatabaseAdapter;
32
+ jwtSecret: string;
33
+ corsOrigins?: string[];
34
+ /** Requests per minute per IP (default: 120) */
35
+ rateLimit?: number;
36
+ /** Trusted proxy IPs for X-Forwarded-For */
37
+ trustedProxies?: string[];
38
+ /** Enable verbose request logging (default: true) */
39
+ logging?: boolean;
40
+ }
41
+
42
+ export interface ServerInstance {
43
+ app: Hono;
44
+ start: () => Promise<{ close: () => void }>;
45
+ healthMonitor: HealthMonitor;
46
+ }
47
+
48
+ export function createServer(config: ServerConfig): ServerInstance {
49
+ const app = new Hono();
50
+
51
+ // ─── DB Circuit Breaker ──────────────────────────────
52
+
53
+ const dbBreaker = new CircuitBreaker({
54
+ failureThreshold: 5,
55
+ recoveryTimeMs: 30_000,
56
+ timeout: 10_000,
57
+ });
58
+
59
+ // ─── Health Monitor ──────────────────────────────────
60
+
61
+ const healthMonitor = new HealthMonitor(
62
+ async () => {
63
+ // Simple connectivity check
64
+ await config.db.getStats();
65
+ },
66
+ { intervalMs: 30_000, timeoutMs: 5_000, unhealthyThreshold: 3 },
67
+ );
68
+
69
+ healthMonitor.onStatusChange((healthy) => {
70
+ console.log(
71
+ `[${new Date().toISOString()}] ${healthy ? '✅' : '❌'} Database health: ${healthy ? 'healthy' : 'unhealthy'}`,
72
+ );
73
+ });
74
+
75
+ // ─── Global Middleware ───────────────────────────────
76
+
77
+ // Request ID (first — everything references it)
78
+ app.use('*', requestIdMiddleware());
79
+
80
+ // Error handler (wraps everything below)
81
+ app.use('*', errorHandler());
82
+
83
+ // Security headers
84
+ app.use('*', securityHeaders());
85
+
86
+ // CORS
87
+ app.use('*', cors({
88
+ origin: config.corsOrigins || '*',
89
+ credentials: true,
90
+ allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-Id'],
91
+ exposeHeaders: ['X-Request-Id', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'Retry-After'],
92
+ }));
93
+
94
+ // Rate limiting
95
+ app.use('*', rateLimiter({
96
+ limit: config.rateLimit ?? 120,
97
+ windowSec: 60,
98
+ skipPaths: ['/health', '/ready'],
99
+ }));
100
+
101
+ // Request logging
102
+ if (config.logging !== false) {
103
+ app.use('*', requestLogger());
104
+ }
105
+
106
+ // ─── Health Endpoints ────────────────────────────────
107
+
108
+ app.get('/health', (c) => c.json({
109
+ status: 'ok',
110
+ version: '0.2.0',
111
+ uptime: process.uptime(),
112
+ }));
113
+
114
+ app.get('/ready', async (c) => {
115
+ const dbHealthy = healthMonitor.isHealthy();
116
+ const status = dbHealthy ? 200 : 503;
117
+ return c.json({
118
+ ready: dbHealthy,
119
+ checks: {
120
+ database: dbHealthy ? 'ok' : 'unhealthy',
121
+ circuitBreaker: dbBreaker.getState(),
122
+ },
123
+ }, status);
124
+ });
125
+
126
+ // ─── Auth Routes (public) ───────────────────────────
127
+
128
+ const authRoutes = createAuthRoutes(config.db, config.jwtSecret);
129
+ app.route('/auth', authRoutes);
130
+
131
+ // ─── Protected API Routes ───────────────────────────
132
+
133
+ const api = new Hono();
134
+
135
+ // Authentication middleware
136
+ api.use('*', async (c, next) => {
137
+ // Check API key first
138
+ const apiKeyHeader = c.req.header('X-API-Key');
139
+ if (apiKeyHeader) {
140
+ const key = await dbBreaker.execute(() => config.db.validateApiKey(apiKeyHeader));
141
+ if (!key) return c.json({ error: 'Invalid API key' }, 401);
142
+ c.set('userId' as any, key.createdBy);
143
+ c.set('authType' as any, 'api-key');
144
+ c.set('apiKeyScopes' as any, key.scopes);
145
+ return next();
146
+ }
147
+
148
+ // JWT auth
149
+ const authHeader = c.req.header('Authorization');
150
+ if (!authHeader?.startsWith('Bearer ')) {
151
+ return c.json({ error: 'Authentication required' }, 401);
152
+ }
153
+
154
+ try {
155
+ const { jwtVerify } = await import('jose');
156
+ const secret = new TextEncoder().encode(config.jwtSecret);
157
+ const { payload } = await jwtVerify(authHeader.slice(7), secret);
158
+ c.set('userId' as any, payload.sub);
159
+ c.set('userRole' as any, payload.role);
160
+ c.set('authType' as any, 'jwt');
161
+ return next();
162
+ } catch {
163
+ return c.json({ error: 'Invalid or expired token' }, 401);
164
+ }
165
+ });
166
+
167
+ // Audit logging on all API mutations
168
+ api.use('*', auditLogger(config.db));
169
+
170
+ // Admin routes
171
+ const adminRoutes = createAdminRoutes(config.db);
172
+ api.route('/', adminRoutes);
173
+
174
+ // Engine routes (skills, permissions, deployment, approvals, lifecycle, KB, etc.)
175
+ // Loaded lazily on first request to avoid top-level await.
176
+ // On first hit, also initializes the EngineDatabase and runs engine migrations.
177
+ let engineInitialized = false;
178
+ api.all('/engine/*', async (c, next) => {
179
+ try {
180
+ const { engineRoutes, setEngineDb } = await import('./engine/routes.js');
181
+ const { EngineDatabase } = await import('./engine/db-adapter.js');
182
+
183
+ // Initialize engine DB on first request
184
+ if (!engineInitialized) {
185
+ // Determine dialect from the adapter
186
+ const dbType = (config.db as any).type || (config.db as any).config?.type || 'sqlite';
187
+ const dialectMap: Record<string, string> = {
188
+ sqlite: 'sqlite', postgres: 'postgres', postgresql: 'postgres',
189
+ mysql: 'mysql', mariadb: 'mysql', turso: 'turso', libsql: 'turso',
190
+ mongodb: 'mongodb', dynamodb: 'dynamodb',
191
+ };
192
+ const dialect = (dialectMap[dbType] || 'sqlite') as any;
193
+
194
+ // Create an EngineDB wrapper around the existing DatabaseAdapter
195
+ const engineDbWrapper = {
196
+ run: async (sql: string, params?: any[]) => {
197
+ await (config.db as any).run?.(sql, params) ?? (config.db as any).query?.(sql, params);
198
+ },
199
+ get: async <T = any>(sql: string, params?: any[]): Promise<T | undefined> => {
200
+ if ((config.db as any).get) return (config.db as any).get(sql, params);
201
+ const rows = await (config.db as any).query?.(sql, params) ?? [];
202
+ return rows[0];
203
+ },
204
+ all: async <T = any>(sql: string, params?: any[]): Promise<T[]> => {
205
+ if ((config.db as any).all) return (config.db as any).all(sql, params);
206
+ return await (config.db as any).query?.(sql, params) ?? [];
207
+ },
208
+ };
209
+
210
+ const engineDb = new EngineDatabase(engineDbWrapper, dialect, (config.db as any).rawDriver);
211
+ const migrationResult = await engineDb.migrate();
212
+ console.log(`[engine] Migrations: ${migrationResult.applied} applied, ${migrationResult.total} total`);
213
+ setEngineDb(engineDb);
214
+ engineInitialized = true;
215
+ }
216
+
217
+ // Forward to engine routes
218
+ const subPath = c.req.path.replace(/^\/api\/engine/, '') || '/';
219
+ const subReq = new Request(new URL(subPath, 'http://localhost'), {
220
+ method: c.req.method,
221
+ headers: c.req.raw.headers,
222
+ body: c.req.method !== 'GET' && c.req.method !== 'HEAD' ? c.req.raw.body : undefined,
223
+ });
224
+ return engineRoutes.fetch(subReq);
225
+ } catch (e: any) {
226
+ console.error('[engine] Error:', e.message);
227
+ return c.json({ error: 'Engine module not available', detail: e.message }, 501);
228
+ }
229
+ });
230
+
231
+ app.route('/api', api);
232
+
233
+ // ─── Dashboard (Admin UI) ─────────────────────────────
234
+
235
+ let dashboardHtml: string | null = null;
236
+ function getDashboardHtml(): string {
237
+ if (!dashboardHtml) {
238
+ try {
239
+ const dir = dirname(fileURLToPath(import.meta.url));
240
+ dashboardHtml = readFileSync(join(dir, 'dashboard', 'index.html'), 'utf-8');
241
+ } catch {
242
+ // Fallback: try relative to cwd
243
+ try {
244
+ dashboardHtml = readFileSync(join(process.cwd(), 'node_modules', '@agenticmail', 'enterprise', 'dist', 'dashboard', 'index.html'), 'utf-8');
245
+ } catch {
246
+ dashboardHtml = '<html><body><h1>Dashboard not found</h1><p>The dashboard HTML file could not be located.</p></body></html>';
247
+ }
248
+ }
249
+ }
250
+ return dashboardHtml;
251
+ }
252
+
253
+ app.get('/', (c) => c.redirect('/dashboard'));
254
+ app.get('/dashboard', (c) => c.html(getDashboardHtml()));
255
+ app.get('/dashboard/*', (c) => c.html(getDashboardHtml()));
256
+
257
+ // ─── 404 Handler ─────────────────────────────────────
258
+
259
+ app.notFound((c) => {
260
+ return c.json({ error: 'Not found', path: c.req.path }, 404);
261
+ });
262
+
263
+ // ─── Server Start ────────────────────────────────────
264
+
265
+ return {
266
+ app,
267
+ healthMonitor,
268
+ start: () => {
269
+ return new Promise((resolve) => {
270
+ const server = serve(
271
+ { fetch: app.fetch, port: config.port },
272
+ (info) => {
273
+ console.log(`\n🏢 AgenticMail Enterprise`);
274
+ console.log(` API: http://localhost:${info.port}/api`);
275
+ console.log(` Auth: http://localhost:${info.port}/auth`);
276
+ console.log(` Health: http://localhost:${info.port}/health`);
277
+ console.log('');
278
+
279
+ // Start health monitoring
280
+ healthMonitor.start();
281
+
282
+ // Graceful shutdown
283
+ const shutdown = () => {
284
+ console.log('\n⏳ Shutting down gracefully...');
285
+ healthMonitor.stop();
286
+ server.close(() => {
287
+ config.db.disconnect().then(() => {
288
+ console.log('✅ Shutdown complete');
289
+ process.exit(0);
290
+ });
291
+ });
292
+ // Force exit after 10s
293
+ setTimeout(() => { process.exit(1); }, 10_000).unref();
294
+ };
295
+
296
+ process.on('SIGINT', shutdown);
297
+ process.on('SIGTERM', shutdown);
298
+
299
+ resolve({
300
+ close: () => {
301
+ healthMonitor.stop();
302
+ server.close();
303
+ },
304
+ });
305
+ },
306
+ );
307
+ });
308
+ },
309
+ };
310
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src"
12
+ },
13
+ "include": ["src"]
14
+ }