@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,368 @@
1
+ /**
2
+ * Fly.io Deployment Module
3
+ *
4
+ * Provisions isolated Fly.io apps for enterprise customers
5
+ * using the Machines API (REST, no flyctl needed).
6
+ *
7
+ * Each customer gets:
8
+ * - Isolated Fly.io machine
9
+ * - <subdomain>.agenticmail.cloud domain
10
+ * - Auto-TLS via Fly.io
11
+ * - Secrets injected as env vars
12
+ */
13
+
14
+ import { withRetry } from '../lib/resilience.js';
15
+
16
+ const FLY_API = 'https://api.machines.dev';
17
+ const DEFAULT_IMAGE = 'agenticmail/enterprise:latest';
18
+
19
+ export interface FlyConfig {
20
+ /** Fly.io API token */
21
+ apiToken: string;
22
+ /** Fly.io organization slug (default: 'personal') */
23
+ org?: string;
24
+ /** Docker image to deploy (default: agenticmail/enterprise:latest) */
25
+ image?: string;
26
+ /** Regions to deploy to */
27
+ regions?: string[];
28
+ }
29
+
30
+ export interface AppConfig {
31
+ subdomain: string;
32
+ dbType: string;
33
+ dbConnectionString: string;
34
+ jwtSecret: string;
35
+ smtpHost?: string;
36
+ smtpPort?: number;
37
+ smtpUser?: string;
38
+ smtpPass?: string;
39
+ /** RAM in MB (default: 256) */
40
+ memoryMb?: number;
41
+ /** CPU kind (default: shared, 1 CPU) */
42
+ cpuKind?: 'shared' | 'performance';
43
+ cpus?: number;
44
+ }
45
+
46
+ export interface DeployResult {
47
+ appName: string;
48
+ url: string;
49
+ ipv4?: string;
50
+ ipv6?: string;
51
+ region: string;
52
+ machineId: string;
53
+ status: 'created' | 'started' | 'error';
54
+ error?: string;
55
+ }
56
+
57
+ async function flyRequest(
58
+ path: string,
59
+ opts: { method?: string; body?: any; apiToken: string },
60
+ ): Promise<any> {
61
+ const resp = await fetch(`${FLY_API}${path}`, {
62
+ method: opts.method || 'GET',
63
+ headers: {
64
+ Authorization: `Bearer ${opts.apiToken}`,
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
68
+ signal: AbortSignal.timeout(30_000),
69
+ });
70
+
71
+ if (!resp.ok) {
72
+ const text = await resp.text().catch(() => 'Unknown error');
73
+ throw new Error(`Fly.io API error (${resp.status}): ${text}`);
74
+ }
75
+
76
+ return resp.json();
77
+ }
78
+
79
+ /**
80
+ * Create a new Fly.io app.
81
+ */
82
+ async function createApp(name: string, fly: FlyConfig): Promise<void> {
83
+ await flyRequest('/v1/apps', {
84
+ method: 'POST',
85
+ apiToken: fly.apiToken,
86
+ body: {
87
+ app_name: name,
88
+ org_slug: fly.org || 'personal',
89
+ },
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Set secrets (environment variables) on a Fly.io app.
95
+ */
96
+ async function setSecrets(
97
+ appName: string,
98
+ secrets: Record<string, string>,
99
+ fly: FlyConfig,
100
+ ): Promise<void> {
101
+ // Fly Machines API doesn't have a direct "set secrets" endpoint
102
+ // like flyctl does. Secrets are passed as env vars in machine config.
103
+ // We store them for use when creating the machine.
104
+ // This is a no-op — secrets are injected in createMachine().
105
+ }
106
+
107
+ /**
108
+ * Create and start a Fly.io machine.
109
+ */
110
+ async function createMachine(
111
+ appName: string,
112
+ config: AppConfig,
113
+ fly: FlyConfig,
114
+ ): Promise<{ id: string; region: string }> {
115
+ const region = fly.regions?.[0] || 'iad';
116
+
117
+ const env: Record<string, string> = {
118
+ PORT: '3000',
119
+ NODE_ENV: 'production',
120
+ DATABASE_TYPE: config.dbType,
121
+ DATABASE_URL: config.dbConnectionString,
122
+ JWT_SECRET: config.jwtSecret,
123
+ };
124
+
125
+ if (config.smtpHost) env.SMTP_HOST = config.smtpHost;
126
+ if (config.smtpPort) env.SMTP_PORT = String(config.smtpPort);
127
+ if (config.smtpUser) env.SMTP_USER = config.smtpUser;
128
+ if (config.smtpPass) env.SMTP_PASS = config.smtpPass;
129
+
130
+ const result = await flyRequest(`/v1/apps/${appName}/machines`, {
131
+ method: 'POST',
132
+ apiToken: fly.apiToken,
133
+ body: {
134
+ name: `${appName}-web`,
135
+ region,
136
+ config: {
137
+ image: fly.image || DEFAULT_IMAGE,
138
+ env,
139
+ services: [
140
+ {
141
+ ports: [
142
+ { port: 443, handlers: ['tls', 'http'] },
143
+ { port: 80, handlers: ['http'] },
144
+ ],
145
+ protocol: 'tcp',
146
+ internal_port: 3000,
147
+ concurrency: {
148
+ type: 'connections',
149
+ hard_limit: 100,
150
+ soft_limit: 80,
151
+ },
152
+ },
153
+ ],
154
+ checks: {
155
+ health: {
156
+ type: 'http',
157
+ port: 3000,
158
+ path: '/health',
159
+ interval: '30s',
160
+ timeout: '5s',
161
+ grace_period: '10s',
162
+ },
163
+ },
164
+ guest: {
165
+ cpu_kind: config.cpuKind || 'shared',
166
+ cpus: config.cpus || 1,
167
+ memory_mb: config.memoryMb || 256,
168
+ },
169
+ auto_destroy: false,
170
+ restart: {
171
+ policy: 'always',
172
+ max_retries: 5,
173
+ },
174
+ },
175
+ },
176
+ });
177
+
178
+ return { id: result.id, region: result.region || region };
179
+ }
180
+
181
+ /**
182
+ * Allocate a dedicated IPv4 address for the app.
183
+ */
184
+ async function allocateIp(appName: string, fly: FlyConfig): Promise<{ v4?: string; v6?: string }> {
185
+ try {
186
+ // Fly.io GraphQL API for IP allocation
187
+ const resp = await fetch('https://api.fly.io/graphql', {
188
+ method: 'POST',
189
+ headers: {
190
+ Authorization: `Bearer ${fly.apiToken}`,
191
+ 'Content-Type': 'application/json',
192
+ },
193
+ body: JSON.stringify({
194
+ query: `mutation($input: AllocateIPAddressInput!) {
195
+ allocateIpAddress(input: $input) {
196
+ ipAddress { id address type region createdAt }
197
+ }
198
+ }`,
199
+ variables: {
200
+ input: { appId: appName, type: 'v4', region: '' },
201
+ },
202
+ }),
203
+ signal: AbortSignal.timeout(15_000),
204
+ });
205
+ const data = await resp.json();
206
+ const v4 = data?.data?.allocateIpAddress?.ipAddress?.address;
207
+ return { v4 };
208
+ } catch {
209
+ return {};
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Set up a custom certificate for the app.
215
+ */
216
+ async function addCertificate(
217
+ appName: string,
218
+ hostname: string,
219
+ fly: FlyConfig,
220
+ ): Promise<void> {
221
+ await fetch('https://api.fly.io/graphql', {
222
+ method: 'POST',
223
+ headers: {
224
+ Authorization: `Bearer ${fly.apiToken}`,
225
+ 'Content-Type': 'application/json',
226
+ },
227
+ body: JSON.stringify({
228
+ query: `mutation($appId: ID!, $hostname: String!) {
229
+ addCertificate(appId: $appId, hostname: $hostname) {
230
+ certificate { hostname configured }
231
+ }
232
+ }`,
233
+ variables: { appId: appName, hostname },
234
+ }),
235
+ signal: AbortSignal.timeout(15_000),
236
+ });
237
+ }
238
+
239
+ // ─── Main Deploy Function ────────────────────────────────
240
+
241
+ export async function deployToFly(
242
+ config: AppConfig,
243
+ fly: FlyConfig,
244
+ ): Promise<DeployResult> {
245
+ const appName = `am-${config.subdomain}`;
246
+ const domain = `${config.subdomain}.agenticmail.cloud`;
247
+
248
+ try {
249
+ // Step 1: Create app
250
+ console.log(` Creating app: ${appName}...`);
251
+ await withRetry(() => createApp(appName, fly), {
252
+ maxAttempts: 2,
253
+ retryableErrors: (err) => !err.message.includes('already exists'),
254
+ });
255
+
256
+ // Step 2: Create machine
257
+ console.log(` Deploying machine...`);
258
+ const machine = await withRetry(() => createMachine(appName, config, fly), {
259
+ maxAttempts: 3,
260
+ baseDelayMs: 2000,
261
+ });
262
+
263
+ // Step 3: Allocate IP
264
+ console.log(` Allocating IP address...`);
265
+ const ips = await allocateIp(appName, fly);
266
+
267
+ // Step 4: Add TLS certificate
268
+ console.log(` Setting up TLS for ${domain}...`);
269
+ await addCertificate(appName, domain, fly).catch(() => {
270
+ // Non-critical — can be added later
271
+ });
272
+
273
+ return {
274
+ appName,
275
+ url: `https://${domain}`,
276
+ ipv4: ips.v4,
277
+ ipv6: ips.v6,
278
+ region: machine.region,
279
+ machineId: machine.id,
280
+ status: 'started',
281
+ };
282
+ } catch (err: any) {
283
+ return {
284
+ appName,
285
+ url: `https://${domain}`,
286
+ region: fly.regions?.[0] || 'iad',
287
+ machineId: '',
288
+ status: 'error',
289
+ error: err.message,
290
+ };
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Check the status of a deployed app.
296
+ */
297
+ export async function getAppStatus(
298
+ appName: string,
299
+ fly: FlyConfig,
300
+ ): Promise<{ running: boolean; machines: any[] }> {
301
+ try {
302
+ const machines = await flyRequest(`/v1/apps/${appName}/machines`, {
303
+ apiToken: fly.apiToken,
304
+ });
305
+ const running = machines.some((m: any) => m.state === 'started');
306
+ return { running, machines };
307
+ } catch {
308
+ return { running: false, machines: [] };
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Destroy a deployed app and all its machines.
314
+ */
315
+ export async function destroyApp(
316
+ appName: string,
317
+ fly: FlyConfig,
318
+ ): Promise<void> {
319
+ // List and stop all machines first
320
+ const { machines } = await getAppStatus(appName, fly);
321
+ for (const m of machines) {
322
+ try {
323
+ await flyRequest(`/v1/apps/${appName}/machines/${m.id}/stop`, {
324
+ method: 'POST',
325
+ apiToken: fly.apiToken,
326
+ });
327
+ } catch { /* ignore */ }
328
+ }
329
+
330
+ // Delete app via GraphQL
331
+ await fetch('https://api.fly.io/graphql', {
332
+ method: 'POST',
333
+ headers: {
334
+ Authorization: `Bearer ${fly.apiToken}`,
335
+ 'Content-Type': 'application/json',
336
+ },
337
+ body: JSON.stringify({
338
+ query: `mutation($appId: ID!) { deleteApp(appId: $appId) { organization { id } } }`,
339
+ variables: { appId: appName },
340
+ }),
341
+ signal: AbortSignal.timeout(15_000),
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Scale a machine (change CPU/memory).
347
+ */
348
+ export async function scaleMachine(
349
+ appName: string,
350
+ machineId: string,
351
+ opts: { memoryMb?: number; cpuKind?: string; cpus?: number },
352
+ fly: FlyConfig,
353
+ ): Promise<void> {
354
+ const machine = await flyRequest(`/v1/apps/${appName}/machines/${machineId}`, {
355
+ apiToken: fly.apiToken,
356
+ });
357
+
358
+ const config = machine.config || {};
359
+ if (opts.memoryMb) config.guest = { ...config.guest, memory_mb: opts.memoryMb };
360
+ if (opts.cpuKind) config.guest = { ...config.guest, cpu_kind: opts.cpuKind };
361
+ if (opts.cpus) config.guest = { ...config.guest, cpus: opts.cpus };
362
+
363
+ await flyRequest(`/v1/apps/${appName}/machines/${machineId}`, {
364
+ method: 'PATCH',
365
+ apiToken: fly.apiToken,
366
+ body: { config },
367
+ });
368
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * AgenticMail Cloud (Managed Deployment)
3
+ *
4
+ * Orchestrates deployment to various targets.
5
+ * "Cloud" mode uses Fly.io under the agenticmail org.
6
+ * Also generates Docker Compose and Fly.toml for self-hosted.
7
+ */
8
+
9
+ import { randomUUID } from 'crypto';
10
+ import { deployToFly, type FlyConfig, type AppConfig } from './fly.js';
11
+
12
+ export interface DeployConfig {
13
+ subdomain: string;
14
+ region?: string;
15
+ plan: 'free' | 'team' | 'enterprise';
16
+ dbType: string;
17
+ dbConnectionString: string;
18
+ jwtSecret: string;
19
+ }
20
+
21
+ export interface DeployResult {
22
+ url: string;
23
+ appName: string;
24
+ region: string;
25
+ status: 'deployed' | 'pending' | 'error';
26
+ error?: string;
27
+ }
28
+
29
+ /**
30
+ * Deploy to AgenticMail Cloud (managed Fly.io).
31
+ *
32
+ * Requires FLY_API_TOKEN env var or explicit token.
33
+ */
34
+ export async function deployToCloud(
35
+ config: DeployConfig,
36
+ flyToken?: string,
37
+ ): Promise<DeployResult> {
38
+ const token = flyToken || process.env.FLY_API_TOKEN;
39
+ if (!token) {
40
+ return {
41
+ url: `https://${config.subdomain}.agenticmail.cloud`,
42
+ appName: `am-${config.subdomain}`,
43
+ region: config.region || 'iad',
44
+ status: 'pending',
45
+ error: 'FLY_API_TOKEN not set. Set it to enable cloud deployment.',
46
+ };
47
+ }
48
+
49
+ const flyConfig: FlyConfig = {
50
+ apiToken: token,
51
+ org: process.env.FLY_ORG || 'agenticmail',
52
+ regions: [config.region || 'iad'],
53
+ };
54
+
55
+ const appConfig: AppConfig = {
56
+ subdomain: config.subdomain,
57
+ dbType: config.dbType,
58
+ dbConnectionString: config.dbConnectionString,
59
+ jwtSecret: config.jwtSecret,
60
+ memoryMb: config.plan === 'free' ? 256 : config.plan === 'team' ? 512 : 1024,
61
+ cpuKind: config.plan === 'enterprise' ? 'performance' : 'shared',
62
+ cpus: config.plan === 'enterprise' ? 2 : 1,
63
+ };
64
+
65
+ const result = await deployToFly(appConfig, flyConfig);
66
+
67
+ return {
68
+ url: result.url,
69
+ appName: result.appName,
70
+ region: result.region,
71
+ status: result.status === 'error' ? 'error' : 'deployed',
72
+ error: result.error,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Generate a Docker Compose file for self-hosted deployment.
78
+ */
79
+ export function generateDockerCompose(opts: {
80
+ dbType: string;
81
+ dbConnectionString: string;
82
+ port: number;
83
+ jwtSecret: string;
84
+ smtpHost?: string;
85
+ smtpPort?: number;
86
+ smtpUser?: string;
87
+ smtpPass?: string;
88
+ }): string {
89
+ const env: string[] = [
90
+ ` - NODE_ENV=production`,
91
+ ` - DATABASE_TYPE=${opts.dbType}`,
92
+ ` - DATABASE_URL=${opts.dbConnectionString}`,
93
+ ` - JWT_SECRET=${opts.jwtSecret}`,
94
+ ` - PORT=3000`,
95
+ ];
96
+
97
+ if (opts.smtpHost) {
98
+ env.push(` - SMTP_HOST=${opts.smtpHost}`);
99
+ env.push(` - SMTP_PORT=${opts.smtpPort || 587}`);
100
+ if (opts.smtpUser) env.push(` - SMTP_USER=${opts.smtpUser}`);
101
+ if (opts.smtpPass) env.push(` - SMTP_PASS=${opts.smtpPass}`);
102
+ }
103
+
104
+ return `# AgenticMail Enterprise — Docker Compose
105
+ # Generated at ${new Date().toISOString()}
106
+ #
107
+ # Usage:
108
+ # docker compose up -d
109
+ # open http://localhost:${opts.port}
110
+
111
+ version: "3.8"
112
+
113
+ services:
114
+ agenticmail:
115
+ image: agenticmail/enterprise:latest
116
+ ports:
117
+ - "${opts.port}:3000"
118
+ environment:
119
+ ${env.join('\n')}
120
+ restart: unless-stopped
121
+ healthcheck:
122
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
123
+ interval: 30s
124
+ timeout: 10s
125
+ retries: 3
126
+ start_period: 15s
127
+ deploy:
128
+ resources:
129
+ limits:
130
+ memory: 512M
131
+ cpus: '1.0'
132
+ reservations:
133
+ memory: 128M
134
+ logging:
135
+ driver: json-file
136
+ options:
137
+ max-size: "10m"
138
+ max-file: "3"
139
+ `;
140
+ }
141
+
142
+ /**
143
+ * Generate a Fly.toml for customer self-deployment.
144
+ */
145
+ export function generateFlyToml(appName: string, region: string): string {
146
+ return `# AgenticMail Enterprise — Fly.io Config
147
+ # Generated at ${new Date().toISOString()}
148
+ #
149
+ # Deploy:
150
+ # fly launch --copy-config
151
+ # fly secrets set DATABASE_URL="..." JWT_SECRET="..."
152
+ # fly deploy
153
+
154
+ app = "${appName}"
155
+ primary_region = "${region}"
156
+
157
+ [build]
158
+ image = "agenticmail/enterprise:latest"
159
+
160
+ [env]
161
+ PORT = "3000"
162
+ NODE_ENV = "production"
163
+
164
+ [http_service]
165
+ internal_port = 3000
166
+ force_https = true
167
+ auto_stop_machines = "stop"
168
+ auto_start_machines = true
169
+ min_machines_running = 1
170
+
171
+ [http_service.concurrency]
172
+ type = "connections"
173
+ hard_limit = 100
174
+ soft_limit = 80
175
+
176
+ [checks]
177
+ [checks.health]
178
+ type = "http"
179
+ port = 3000
180
+ path = "/health"
181
+ interval = "30s"
182
+ timeout = "5s"
183
+ grace_period = "10s"
184
+
185
+ [[vm]]
186
+ size = "shared-cpu-1x"
187
+ memory = "256mb"
188
+ `;
189
+ }
190
+
191
+ /**
192
+ * Generate a Railway deployment config.
193
+ */
194
+ export function generateRailwayConfig(): string {
195
+ return `# AgenticMail Enterprise — Railway Config
196
+ # Generated at ${new Date().toISOString()}
197
+ #
198
+ # Deploy:
199
+ # railway init
200
+ # railway link
201
+ # railway up
202
+
203
+ [build]
204
+ builder = "DOCKERFILE"
205
+ dockerfilePath = "Dockerfile"
206
+
207
+ [deploy]
208
+ healthcheckPath = "/health"
209
+ healthcheckTimeout = 10
210
+ restartPolicyType = "ON_FAILURE"
211
+ restartPolicyMaxRetries = 3
212
+ `;
213
+ }