@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
|
@@ -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
|
+
}
|