@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.
- package/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- 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
|
+
}
|