@agenticmail/enterprise 0.2.2 → 0.3.0

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.
@@ -0,0 +1,48 @@
1
+ // src/db/factory.ts
2
+ var ADAPTER_MAP = {
3
+ postgres: () => import("./postgres-LN7A6MGQ.js").then((m) => m.PostgresAdapter),
4
+ supabase: () => import("./postgres-LN7A6MGQ.js").then((m) => m.PostgresAdapter),
5
+ // Supabase IS Postgres
6
+ neon: () => import("./postgres-LN7A6MGQ.js").then((m) => m.PostgresAdapter),
7
+ // Neon IS Postgres
8
+ cockroachdb: () => import("./postgres-LN7A6MGQ.js").then((m) => m.PostgresAdapter),
9
+ // CockroachDB is PG-compatible
10
+ mysql: () => import("./mysql-RM3S2FV5.js").then((m) => m.MysqlAdapter),
11
+ planetscale: () => import("./mysql-RM3S2FV5.js").then((m) => m.MysqlAdapter),
12
+ // PlanetScale IS MySQL
13
+ mongodb: () => import("./mongodb-ODTXIVPV.js").then((m) => m.MongoAdapter),
14
+ sqlite: () => import("./sqlite-3K5YOZ4K.js").then((m) => m.SqliteAdapter),
15
+ turso: () => import("./turso-LDWODSDI.js").then((m) => m.TursoAdapter),
16
+ dynamodb: () => import("./dynamodb-CCGL2E77.js").then((m) => m.DynamoAdapter)
17
+ };
18
+ async function createAdapter(config) {
19
+ const loader = ADAPTER_MAP[config.type];
20
+ if (!loader) {
21
+ throw new Error(
22
+ `Unsupported database type: "${config.type}". Supported: ${Object.keys(ADAPTER_MAP).join(", ")}`
23
+ );
24
+ }
25
+ const AdapterClass = await loader();
26
+ const adapter = new AdapterClass();
27
+ await adapter.connect(config);
28
+ return adapter;
29
+ }
30
+ function getSupportedDatabases() {
31
+ return [
32
+ { type: "postgres", label: "PostgreSQL", group: "SQL" },
33
+ { type: "mysql", label: "MySQL / MariaDB", group: "SQL" },
34
+ { type: "sqlite", label: "SQLite (embedded, dev/small)", group: "SQL" },
35
+ { type: "mongodb", label: "MongoDB", group: "NoSQL" },
36
+ { type: "turso", label: "Turso (LibSQL, edge)", group: "Edge" },
37
+ { type: "dynamodb", label: "DynamoDB (AWS)", group: "Cloud" },
38
+ { type: "supabase", label: "Supabase (managed Postgres)", group: "Cloud" },
39
+ { type: "neon", label: "Neon (serverless Postgres)", group: "Cloud" },
40
+ { type: "planetscale", label: "PlanetScale (managed MySQL)", group: "Cloud" },
41
+ { type: "cockroachdb", label: "CockroachDB", group: "Distributed" }
42
+ ];
43
+ }
44
+
45
+ export {
46
+ createAdapter,
47
+ getSupportedDatabases
48
+ };
@@ -0,0 +1,255 @@
1
+ // src/lib/resilience.ts
2
+ var DEFAULT_RETRY = {
3
+ maxAttempts: 3,
4
+ baseDelayMs: 500,
5
+ maxDelayMs: 3e4,
6
+ backoffMultiplier: 2
7
+ };
8
+ async function withRetry(fn, opts = {}) {
9
+ const config = { ...DEFAULT_RETRY, ...opts };
10
+ let lastError = new Error("No attempts made");
11
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
12
+ try {
13
+ return await fn();
14
+ } catch (err) {
15
+ lastError = err;
16
+ if (attempt === config.maxAttempts) break;
17
+ if (config.retryableErrors && !config.retryableErrors(err)) break;
18
+ const delay = Math.min(
19
+ config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt - 1) + Math.random() * 200,
20
+ config.maxDelayMs
21
+ );
22
+ config.onRetry?.(attempt, err, delay);
23
+ await sleep(delay);
24
+ }
25
+ }
26
+ throw lastError;
27
+ }
28
+ var CircuitBreaker = class {
29
+ state = "closed";
30
+ failures = 0;
31
+ successes = 0;
32
+ lastFailureTime = 0;
33
+ opts;
34
+ constructor(opts = {}) {
35
+ this.opts = {
36
+ failureThreshold: opts.failureThreshold ?? 5,
37
+ recoveryTimeMs: opts.recoveryTimeMs ?? 3e4,
38
+ successThreshold: opts.successThreshold ?? 2,
39
+ timeout: opts.timeout
40
+ };
41
+ }
42
+ async execute(fn) {
43
+ if (this.state === "open") {
44
+ if (Date.now() - this.lastFailureTime >= this.opts.recoveryTimeMs) {
45
+ this.state = "half-open";
46
+ this.successes = 0;
47
+ } else {
48
+ throw new CircuitOpenError(
49
+ `Circuit breaker is open. Retry after ${this.opts.recoveryTimeMs}ms`
50
+ );
51
+ }
52
+ }
53
+ try {
54
+ const result = this.opts.timeout ? await withTimeout(fn(), this.opts.timeout) : await fn();
55
+ this.onSuccess();
56
+ return result;
57
+ } catch (err) {
58
+ this.onFailure();
59
+ throw err;
60
+ }
61
+ }
62
+ onSuccess() {
63
+ if (this.state === "half-open") {
64
+ this.successes++;
65
+ if (this.successes >= this.opts.successThreshold) {
66
+ this.state = "closed";
67
+ this.failures = 0;
68
+ }
69
+ } else {
70
+ this.failures = 0;
71
+ }
72
+ }
73
+ onFailure() {
74
+ this.failures++;
75
+ this.lastFailureTime = Date.now();
76
+ if (this.failures >= this.opts.failureThreshold) {
77
+ this.state = "open";
78
+ }
79
+ }
80
+ getState() {
81
+ return this.state;
82
+ }
83
+ reset() {
84
+ this.state = "closed";
85
+ this.failures = 0;
86
+ this.successes = 0;
87
+ }
88
+ };
89
+ var CircuitOpenError = class extends Error {
90
+ constructor(message) {
91
+ super(message);
92
+ this.name = "CircuitOpenError";
93
+ }
94
+ };
95
+ var RateLimiter = class {
96
+ tokens;
97
+ lastRefill;
98
+ opts;
99
+ constructor(opts) {
100
+ this.opts = {
101
+ maxTokens: opts.maxTokens,
102
+ refillRate: opts.refillRate,
103
+ refillIntervalMs: opts.refillIntervalMs ?? 1e3
104
+ };
105
+ this.tokens = this.opts.maxTokens;
106
+ this.lastRefill = Date.now();
107
+ }
108
+ /**
109
+ * Try to consume a token. Returns true if allowed, false if rate limited.
110
+ */
111
+ tryConsume(count = 1) {
112
+ this.refill();
113
+ if (this.tokens >= count) {
114
+ this.tokens -= count;
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Get time in ms until next token is available.
121
+ */
122
+ getRetryAfterMs() {
123
+ this.refill();
124
+ if (this.tokens >= 1) return 0;
125
+ const tokensNeeded = 1 - this.tokens;
126
+ return Math.ceil(tokensNeeded / this.opts.refillRate * 1e3);
127
+ }
128
+ refill() {
129
+ const now = Date.now();
130
+ const elapsed = now - this.lastRefill;
131
+ const tokensToAdd = elapsed / 1e3 * this.opts.refillRate;
132
+ this.tokens = Math.min(this.opts.maxTokens, this.tokens + tokensToAdd);
133
+ this.lastRefill = now;
134
+ }
135
+ };
136
+ var KeyedRateLimiter = class {
137
+ limiters = /* @__PURE__ */ new Map();
138
+ opts;
139
+ cleanupTimer = null;
140
+ constructor(opts) {
141
+ this.opts = opts;
142
+ this.cleanupTimer = setInterval(() => this.cleanup(), 5 * 6e4);
143
+ if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
144
+ this.cleanupTimer.unref();
145
+ }
146
+ }
147
+ tryConsume(key, count = 1) {
148
+ let limiter = this.limiters.get(key);
149
+ if (!limiter) {
150
+ limiter = new RateLimiter(this.opts);
151
+ this.limiters.set(key, limiter);
152
+ }
153
+ return limiter.tryConsume(count);
154
+ }
155
+ getRetryAfterMs(key) {
156
+ const limiter = this.limiters.get(key);
157
+ return limiter ? limiter.getRetryAfterMs() : 0;
158
+ }
159
+ cleanup() {
160
+ for (const [key, limiter] of this.limiters) {
161
+ if (limiter.getRetryAfterMs() === 0) {
162
+ this.limiters.delete(key);
163
+ }
164
+ }
165
+ }
166
+ destroy() {
167
+ if (this.cleanupTimer) clearInterval(this.cleanupTimer);
168
+ this.limiters.clear();
169
+ }
170
+ };
171
+ var HealthMonitor = class {
172
+ healthy = true;
173
+ consecutiveFailures = 0;
174
+ consecutiveSuccesses = 0;
175
+ timer = null;
176
+ check;
177
+ opts;
178
+ listeners = [];
179
+ constructor(check, opts = {}) {
180
+ this.check = check;
181
+ this.opts = {
182
+ intervalMs: opts.intervalMs ?? 3e4,
183
+ timeoutMs: opts.timeoutMs ?? 5e3,
184
+ unhealthyThreshold: opts.unhealthyThreshold ?? 3,
185
+ healthyThreshold: opts.healthyThreshold ?? 2
186
+ };
187
+ }
188
+ start() {
189
+ if (this.timer) return;
190
+ this.timer = setInterval(() => this.runCheck(), this.opts.intervalMs);
191
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
192
+ this.timer.unref();
193
+ }
194
+ }
195
+ stop() {
196
+ if (this.timer) {
197
+ clearInterval(this.timer);
198
+ this.timer = null;
199
+ }
200
+ }
201
+ isHealthy() {
202
+ return this.healthy;
203
+ }
204
+ onStatusChange(fn) {
205
+ this.listeners.push(fn);
206
+ }
207
+ async runCheck() {
208
+ try {
209
+ await withTimeout(this.check(), this.opts.timeoutMs);
210
+ this.consecutiveFailures = 0;
211
+ this.consecutiveSuccesses++;
212
+ if (!this.healthy && this.consecutiveSuccesses >= this.opts.healthyThreshold) {
213
+ this.healthy = true;
214
+ this.listeners.forEach((fn) => fn(true));
215
+ }
216
+ } catch {
217
+ this.consecutiveSuccesses = 0;
218
+ this.consecutiveFailures++;
219
+ if (this.healthy && this.consecutiveFailures >= this.opts.unhealthyThreshold) {
220
+ this.healthy = false;
221
+ this.listeners.forEach((fn) => fn(false));
222
+ }
223
+ }
224
+ }
225
+ };
226
+ function sleep(ms) {
227
+ return new Promise((resolve) => setTimeout(resolve, ms));
228
+ }
229
+ function withTimeout(promise, ms) {
230
+ return new Promise((resolve, reject) => {
231
+ const timer = setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms);
232
+ promise.then((v) => {
233
+ clearTimeout(timer);
234
+ resolve(v);
235
+ }).catch((e) => {
236
+ clearTimeout(timer);
237
+ reject(e);
238
+ });
239
+ });
240
+ }
241
+ var counter = 0;
242
+ var prefix = Math.random().toString(36).substring(2, 8);
243
+ function requestId() {
244
+ return `${prefix}-${(++counter).toString(36)}-${Date.now().toString(36)}`;
245
+ }
246
+
247
+ export {
248
+ withRetry,
249
+ CircuitBreaker,
250
+ CircuitOpenError,
251
+ RateLimiter,
252
+ KeyedRateLimiter,
253
+ HealthMonitor,
254
+ requestId
255
+ };
@@ -0,0 +1,338 @@
1
+ import {
2
+ withRetry
3
+ } from "./chunk-JLSQOQ5L.js";
4
+
5
+ // src/deploy/fly.ts
6
+ var FLY_API = "https://api.machines.dev";
7
+ var DEFAULT_IMAGE = "agenticmail/enterprise:latest";
8
+ async function flyRequest(path, opts) {
9
+ const resp = await fetch(`${FLY_API}${path}`, {
10
+ method: opts.method || "GET",
11
+ headers: {
12
+ Authorization: `Bearer ${opts.apiToken}`,
13
+ "Content-Type": "application/json"
14
+ },
15
+ body: opts.body ? JSON.stringify(opts.body) : void 0,
16
+ signal: AbortSignal.timeout(3e4)
17
+ });
18
+ if (!resp.ok) {
19
+ const text = await resp.text().catch(() => "Unknown error");
20
+ throw new Error(`Fly.io API error (${resp.status}): ${text}`);
21
+ }
22
+ return resp.json();
23
+ }
24
+ async function createApp(name, fly) {
25
+ await flyRequest("/v1/apps", {
26
+ method: "POST",
27
+ apiToken: fly.apiToken,
28
+ body: {
29
+ app_name: name,
30
+ org_slug: fly.org || "personal"
31
+ }
32
+ });
33
+ }
34
+ async function createMachine(appName, config, fly) {
35
+ const region = fly.regions?.[0] || "iad";
36
+ const env = {
37
+ PORT: "3000",
38
+ NODE_ENV: "production",
39
+ DATABASE_TYPE: config.dbType,
40
+ DATABASE_URL: config.dbConnectionString,
41
+ JWT_SECRET: config.jwtSecret
42
+ };
43
+ if (config.smtpHost) env.SMTP_HOST = config.smtpHost;
44
+ if (config.smtpPort) env.SMTP_PORT = String(config.smtpPort);
45
+ if (config.smtpUser) env.SMTP_USER = config.smtpUser;
46
+ if (config.smtpPass) env.SMTP_PASS = config.smtpPass;
47
+ const result = await flyRequest(`/v1/apps/${appName}/machines`, {
48
+ method: "POST",
49
+ apiToken: fly.apiToken,
50
+ body: {
51
+ name: `${appName}-web`,
52
+ region,
53
+ config: {
54
+ image: fly.image || DEFAULT_IMAGE,
55
+ env,
56
+ services: [
57
+ {
58
+ ports: [
59
+ { port: 443, handlers: ["tls", "http"] },
60
+ { port: 80, handlers: ["http"] }
61
+ ],
62
+ protocol: "tcp",
63
+ internal_port: 3e3,
64
+ concurrency: {
65
+ type: "connections",
66
+ hard_limit: 100,
67
+ soft_limit: 80
68
+ }
69
+ }
70
+ ],
71
+ checks: {
72
+ health: {
73
+ type: "http",
74
+ port: 3e3,
75
+ path: "/health",
76
+ interval: "30s",
77
+ timeout: "5s",
78
+ grace_period: "10s"
79
+ }
80
+ },
81
+ guest: {
82
+ cpu_kind: config.cpuKind || "shared",
83
+ cpus: config.cpus || 1,
84
+ memory_mb: config.memoryMb || 256
85
+ },
86
+ auto_destroy: false,
87
+ restart: {
88
+ policy: "always",
89
+ max_retries: 5
90
+ }
91
+ }
92
+ }
93
+ });
94
+ return { id: result.id, region: result.region || region };
95
+ }
96
+ async function allocateIp(appName, fly) {
97
+ try {
98
+ const resp = await fetch("https://api.fly.io/graphql", {
99
+ method: "POST",
100
+ headers: {
101
+ Authorization: `Bearer ${fly.apiToken}`,
102
+ "Content-Type": "application/json"
103
+ },
104
+ body: JSON.stringify({
105
+ query: `mutation($input: AllocateIPAddressInput!) {
106
+ allocateIpAddress(input: $input) {
107
+ ipAddress { id address type region createdAt }
108
+ }
109
+ }`,
110
+ variables: {
111
+ input: { appId: appName, type: "v4", region: "" }
112
+ }
113
+ }),
114
+ signal: AbortSignal.timeout(15e3)
115
+ });
116
+ const data = await resp.json();
117
+ const v4 = data?.data?.allocateIpAddress?.ipAddress?.address;
118
+ return { v4 };
119
+ } catch {
120
+ return {};
121
+ }
122
+ }
123
+ async function addCertificate(appName, hostname, fly) {
124
+ await fetch("https://api.fly.io/graphql", {
125
+ method: "POST",
126
+ headers: {
127
+ Authorization: `Bearer ${fly.apiToken}`,
128
+ "Content-Type": "application/json"
129
+ },
130
+ body: JSON.stringify({
131
+ query: `mutation($appId: ID!, $hostname: String!) {
132
+ addCertificate(appId: $appId, hostname: $hostname) {
133
+ certificate { hostname configured }
134
+ }
135
+ }`,
136
+ variables: { appId: appName, hostname }
137
+ }),
138
+ signal: AbortSignal.timeout(15e3)
139
+ });
140
+ }
141
+ async function deployToFly(config, fly) {
142
+ const appName = `am-${config.subdomain}`;
143
+ const domain = `${config.subdomain}.agenticmail.cloud`;
144
+ try {
145
+ console.log(` Creating app: ${appName}...`);
146
+ await withRetry(() => createApp(appName, fly), {
147
+ maxAttempts: 2,
148
+ retryableErrors: (err) => !err.message.includes("already exists")
149
+ });
150
+ console.log(` Deploying machine...`);
151
+ const machine = await withRetry(() => createMachine(appName, config, fly), {
152
+ maxAttempts: 3,
153
+ baseDelayMs: 2e3
154
+ });
155
+ console.log(` Allocating IP address...`);
156
+ const ips = await allocateIp(appName, fly);
157
+ console.log(` Setting up TLS for ${domain}...`);
158
+ await addCertificate(appName, domain, fly).catch(() => {
159
+ });
160
+ return {
161
+ appName,
162
+ url: `https://${domain}`,
163
+ ipv4: ips.v4,
164
+ ipv6: ips.v6,
165
+ region: machine.region,
166
+ machineId: machine.id,
167
+ status: "started"
168
+ };
169
+ } catch (err) {
170
+ return {
171
+ appName,
172
+ url: `https://${domain}`,
173
+ region: fly.regions?.[0] || "iad",
174
+ machineId: "",
175
+ status: "error",
176
+ error: err.message
177
+ };
178
+ }
179
+ }
180
+
181
+ // src/deploy/managed.ts
182
+ async function deployToCloud(config, flyToken) {
183
+ const token = flyToken || process.env.FLY_API_TOKEN;
184
+ if (!token) {
185
+ return {
186
+ url: `https://${config.subdomain}.agenticmail.cloud`,
187
+ appName: `am-${config.subdomain}`,
188
+ region: config.region || "iad",
189
+ status: "pending",
190
+ error: "FLY_API_TOKEN not set. Set it to enable cloud deployment."
191
+ };
192
+ }
193
+ const flyConfig = {
194
+ apiToken: token,
195
+ org: process.env.FLY_ORG || "agenticmail",
196
+ regions: [config.region || "iad"]
197
+ };
198
+ const appConfig = {
199
+ subdomain: config.subdomain,
200
+ dbType: config.dbType,
201
+ dbConnectionString: config.dbConnectionString,
202
+ jwtSecret: config.jwtSecret,
203
+ memoryMb: config.plan === "free" ? 256 : config.plan === "team" ? 512 : 1024,
204
+ cpuKind: config.plan === "enterprise" ? "performance" : "shared",
205
+ cpus: config.plan === "enterprise" ? 2 : 1
206
+ };
207
+ const result = await deployToFly(appConfig, flyConfig);
208
+ return {
209
+ url: result.url,
210
+ appName: result.appName,
211
+ region: result.region,
212
+ status: result.status === "error" ? "error" : "deployed",
213
+ error: result.error
214
+ };
215
+ }
216
+ function generateDockerCompose(opts) {
217
+ const env = [
218
+ ` - NODE_ENV=production`,
219
+ ` - DATABASE_TYPE=${opts.dbType}`,
220
+ ` - DATABASE_URL=${opts.dbConnectionString}`,
221
+ ` - JWT_SECRET=${opts.jwtSecret}`,
222
+ ` - PORT=3000`
223
+ ];
224
+ if (opts.smtpHost) {
225
+ env.push(` - SMTP_HOST=${opts.smtpHost}`);
226
+ env.push(` - SMTP_PORT=${opts.smtpPort || 587}`);
227
+ if (opts.smtpUser) env.push(` - SMTP_USER=${opts.smtpUser}`);
228
+ if (opts.smtpPass) env.push(` - SMTP_PASS=${opts.smtpPass}`);
229
+ }
230
+ return `# AgenticMail Enterprise \u2014 Docker Compose
231
+ # Generated at ${(/* @__PURE__ */ new Date()).toISOString()}
232
+ #
233
+ # Usage:
234
+ # docker compose up -d
235
+ # open http://localhost:${opts.port}
236
+
237
+ version: "3.8"
238
+
239
+ services:
240
+ agenticmail:
241
+ image: agenticmail/enterprise:latest
242
+ ports:
243
+ - "${opts.port}:3000"
244
+ environment:
245
+ ${env.join("\n")}
246
+ restart: unless-stopped
247
+ healthcheck:
248
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
249
+ interval: 30s
250
+ timeout: 10s
251
+ retries: 3
252
+ start_period: 15s
253
+ deploy:
254
+ resources:
255
+ limits:
256
+ memory: 512M
257
+ cpus: '1.0'
258
+ reservations:
259
+ memory: 128M
260
+ logging:
261
+ driver: json-file
262
+ options:
263
+ max-size: "10m"
264
+ max-file: "3"
265
+ `;
266
+ }
267
+ function generateFlyToml(appName, region) {
268
+ return `# AgenticMail Enterprise \u2014 Fly.io Config
269
+ # Generated at ${(/* @__PURE__ */ new Date()).toISOString()}
270
+ #
271
+ # Deploy:
272
+ # fly launch --copy-config
273
+ # fly secrets set DATABASE_URL="..." JWT_SECRET="..."
274
+ # fly deploy
275
+
276
+ app = "${appName}"
277
+ primary_region = "${region}"
278
+
279
+ [build]
280
+ image = "agenticmail/enterprise:latest"
281
+
282
+ [env]
283
+ PORT = "3000"
284
+ NODE_ENV = "production"
285
+
286
+ [http_service]
287
+ internal_port = 3000
288
+ force_https = true
289
+ auto_stop_machines = "stop"
290
+ auto_start_machines = true
291
+ min_machines_running = 1
292
+
293
+ [http_service.concurrency]
294
+ type = "connections"
295
+ hard_limit = 100
296
+ soft_limit = 80
297
+
298
+ [checks]
299
+ [checks.health]
300
+ type = "http"
301
+ port = 3000
302
+ path = "/health"
303
+ interval = "30s"
304
+ timeout = "5s"
305
+ grace_period = "10s"
306
+
307
+ [[vm]]
308
+ size = "shared-cpu-1x"
309
+ memory = "256mb"
310
+ `;
311
+ }
312
+ function generateRailwayConfig() {
313
+ return `# AgenticMail Enterprise \u2014 Railway Config
314
+ # Generated at ${(/* @__PURE__ */ new Date()).toISOString()}
315
+ #
316
+ # Deploy:
317
+ # railway init
318
+ # railway link
319
+ # railway up
320
+
321
+ [build]
322
+ builder = "DOCKERFILE"
323
+ dockerfilePath = "Dockerfile"
324
+
325
+ [deploy]
326
+ healthcheckPath = "/health"
327
+ healthcheckTimeout = 10
328
+ restartPolicyType = "ON_FAILURE"
329
+ restartPolicyMaxRetries = 3
330
+ `;
331
+ }
332
+
333
+ export {
334
+ deployToCloud,
335
+ generateDockerCompose,
336
+ generateFlyToml,
337
+ generateRailwayConfig
338
+ };