@agenticmail/enterprise 0.5.190 → 0.5.192

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/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ Skill Development:
58
58
  break;
59
59
  case "setup":
60
60
  default:
61
- import("./setup-66BT2YBE.js").then((m) => m.runSetupWizard()).catch(fatal);
61
+ import("./setup-NFCGCG7K.js").then((m) => m.runSetupWizard()).catch(fatal);
62
62
  break;
63
63
  }
64
64
  function fatal(err) {
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  provision,
9
9
  runSetupWizard
10
- } from "./chunk-3UERPKPG.js";
10
+ } from "./chunk-WOD3QSGX.js";
11
11
  import {
12
12
  AgenticMailManager,
13
13
  GoogleEmailProvider,
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-WOD3QSGX.js";
10
+ import "./chunk-VQQ4SYYQ.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.190",
3
+ "version": "0.5.192",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,9 +49,11 @@
49
49
  "bcryptjs": "^2.4.3",
50
50
  "chalk": "^5.0.0",
51
51
  "hono": "^4.0.0",
52
+ "imapflow": "^1.2.10",
52
53
  "inquirer": "^9.0.0",
53
54
  "jose": "^5.0.0",
54
55
  "nanoid": "^5.0.0",
56
+ "nodemailer": "^8.0.1",
55
57
  "openai": "^4.77.0",
56
58
  "ora": "^8.0.0",
57
59
  "playwright": "^1.58.2",
@@ -72,7 +74,7 @@
72
74
  },
73
75
  "peerDependencies": {
74
76
  "playwright-core": ">=1.40.0",
75
- "ws": ">=8.0.0"
77
+ "ws": "^8.19.0"
76
78
  },
77
79
  "peerDependenciesMeta": {
78
80
  "playwright-core": {
@@ -2,12 +2,29 @@
2
2
  * Setup Wizard — Step 3: Deployment Target
3
3
  *
4
4
  * Choose where the enterprise server will run.
5
+ * Includes Cloudflare Tunnel as recommended self-hosted option —
6
+ * handles install, login, tunnel creation, DNS, and PM2 in one flow.
5
7
  */
6
8
 
7
- export type DeployTarget = 'cloud' | 'fly' | 'railway' | 'docker' | 'local';
9
+ import { execSync, exec as execCb } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { homedir, platform, arch } from 'os';
14
+
15
+ const execP = promisify(execCb);
16
+
17
+ export type DeployTarget = 'cloud' | 'cloudflare-tunnel' | 'fly' | 'railway' | 'docker' | 'local';
8
18
 
9
19
  export interface DeploymentSelection {
10
20
  target: DeployTarget;
21
+ /** Populated when target is 'cloudflare-tunnel' */
22
+ tunnel?: {
23
+ tunnelId: string;
24
+ domain: string;
25
+ port: number;
26
+ tunnelName: string;
27
+ };
11
28
  }
12
29
 
13
30
  export async function promptDeployment(
@@ -27,6 +44,10 @@ export async function promptDeployment(
27
44
  name: `AgenticMail Cloud ${chalk.dim('(managed, instant URL)')}`,
28
45
  value: 'cloud',
29
46
  },
47
+ {
48
+ name: `Cloudflare Tunnel ${chalk.green('← recommended')} ${chalk.dim('(self-hosted, free, no ports)')}`,
49
+ value: 'cloudflare-tunnel',
50
+ },
30
51
  {
31
52
  name: `Fly.io ${chalk.dim('(your account)')}`,
32
53
  value: 'fly',
@@ -46,5 +67,249 @@ export async function promptDeployment(
46
67
  ],
47
68
  }]);
48
69
 
70
+ if (deployTarget === 'cloudflare-tunnel') {
71
+ const tunnel = await runTunnelSetup(inquirer, chalk);
72
+ return { target: deployTarget, tunnel };
73
+ }
74
+
49
75
  return { target: deployTarget };
50
76
  }
77
+
78
+ // ─── Cloudflare Tunnel Interactive Setup ────────────
79
+
80
+ async function runTunnelSetup(
81
+ inquirer: any,
82
+ chalk: any,
83
+ ): Promise<DeploymentSelection['tunnel']> {
84
+ console.log('');
85
+ console.log(chalk.bold(' Cloudflare Tunnel Setup'));
86
+ console.log(chalk.dim(' Exposes your local server to the internet via Cloudflare.'));
87
+ console.log(chalk.dim(' No open ports, free TLS, auto-DNS.\n'));
88
+
89
+ // ── Step 1: Check / Install cloudflared ─────────
90
+ console.log(chalk.bold(' 1. Cloudflared CLI'));
91
+
92
+ let installed = false;
93
+ let version = '';
94
+ try {
95
+ version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
96
+ installed = true;
97
+ } catch { /* not installed */ }
98
+
99
+ if (installed) {
100
+ console.log(chalk.green(` ✓ Installed (${version})\n`));
101
+ } else {
102
+ console.log(chalk.yellow(' Not installed.'));
103
+ const { doInstall } = await inquirer.prompt([{
104
+ type: 'confirm',
105
+ name: 'doInstall',
106
+ message: 'Install cloudflared now?',
107
+ default: true,
108
+ }]);
109
+
110
+ if (!doInstall) {
111
+ console.log(chalk.red('\n cloudflared is required for tunnel deployment.'));
112
+ console.log(chalk.dim(' Install it manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n'));
113
+ process.exit(1);
114
+ }
115
+
116
+ console.log(chalk.dim(' Installing...'));
117
+ try {
118
+ await installCloudflared();
119
+ version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
120
+ console.log(chalk.green(` ✓ Installed (${version})\n`));
121
+ } catch (err: any) {
122
+ console.log(chalk.red(` ✗ Installation failed: ${err.message}`));
123
+ console.log(chalk.dim(' Install manually and re-run setup.\n'));
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ // ── Step 2: Login to Cloudflare ─────────────────
129
+ console.log(chalk.bold(' 2. Cloudflare Authentication'));
130
+
131
+ const cfDir = join(homedir(), '.cloudflared');
132
+ const certPath = join(cfDir, 'cert.pem');
133
+ const loggedIn = existsSync(certPath);
134
+
135
+ if (loggedIn) {
136
+ console.log(chalk.green(' ✓ Already authenticated\n'));
137
+ } else {
138
+ console.log(chalk.dim(' This will open your browser to authorize Cloudflare.\n'));
139
+ const { doLogin } = await inquirer.prompt([{
140
+ type: 'confirm',
141
+ name: 'doLogin',
142
+ message: 'Open browser to login to Cloudflare?',
143
+ default: true,
144
+ }]);
145
+
146
+ if (!doLogin) {
147
+ console.log(chalk.red('\n Cloudflare auth is required. Run `cloudflared tunnel login` manually.\n'));
148
+ process.exit(1);
149
+ }
150
+
151
+ console.log(chalk.dim(' Waiting for browser authorization...'));
152
+ try {
153
+ await execP('cloudflared tunnel login', { timeout: 120000 });
154
+ console.log(chalk.green(' ✓ Authenticated\n'));
155
+ } catch (err: any) {
156
+ console.log(chalk.red(` ✗ Login failed or timed out: ${err.message}`));
157
+ console.log(chalk.dim(' Complete the browser authorization and try again.\n'));
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ // ── Step 3: Domain + Port ───────────────────────
163
+ console.log(chalk.bold(' 3. Tunnel Configuration'));
164
+
165
+ const { domain, port, tunnelName } = await inquirer.prompt([
166
+ {
167
+ type: 'input',
168
+ name: 'domain',
169
+ message: 'Domain (e.g. dashboard.yourcompany.com):',
170
+ validate: (v: string) => v.includes('.') ? true : 'Enter a valid domain',
171
+ },
172
+ {
173
+ type: 'number',
174
+ name: 'port',
175
+ message: 'Local port:',
176
+ default: 3200,
177
+ },
178
+ {
179
+ type: 'input',
180
+ name: 'tunnelName',
181
+ message: 'Tunnel name:',
182
+ default: 'agenticmail-enterprise',
183
+ },
184
+ ]);
185
+
186
+ // ── Step 4: Create tunnel + DNS + Start ─────────
187
+ console.log('');
188
+ console.log(chalk.bold(' 4. Deploying'));
189
+
190
+ // Create tunnel
191
+ let tunnelId = '';
192
+ try {
193
+ console.log(chalk.dim(' Creating tunnel...'));
194
+ const out = execSync(`cloudflared tunnel create ${tunnelName} 2>&1`, { encoding: 'utf8', timeout: 30000 });
195
+ const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);
196
+ tunnelId = match?.[1] || '';
197
+ console.log(chalk.green(` ✓ Tunnel created: ${tunnelName} (${tunnelId})`));
198
+ } catch (e: any) {
199
+ if (e.message?.includes('already exists') || e.stderr?.includes('already exists')) {
200
+ try {
201
+ const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });
202
+ const tunnels = JSON.parse(listOut);
203
+ const existing = tunnels.find((t: any) => t.name === tunnelName);
204
+ if (existing) {
205
+ tunnelId = existing.id;
206
+ console.log(chalk.green(` ✓ Using existing tunnel: ${tunnelName} (${tunnelId})`));
207
+ }
208
+ } catch {
209
+ console.log(chalk.red(` ✗ Tunnel "${tunnelName}" exists but couldn't read its ID`));
210
+ process.exit(1);
211
+ }
212
+ } else {
213
+ console.log(chalk.red(` ✗ Failed to create tunnel: ${e.message}`));
214
+ process.exit(1);
215
+ }
216
+ }
217
+
218
+ if (!tunnelId) {
219
+ console.log(chalk.red(' ✗ Could not determine tunnel ID'));
220
+ process.exit(1);
221
+ }
222
+
223
+ // Write config
224
+ const config = [
225
+ `tunnel: ${tunnelId}`,
226
+ `credentials-file: ${join(cfDir, tunnelId + '.json')}`,
227
+ '',
228
+ 'ingress:',
229
+ ` - hostname: ${domain}`,
230
+ ` service: http://localhost:${port}`,
231
+ ' - service: http_status:404',
232
+ ].join('\n');
233
+
234
+ writeFileSync(join(cfDir, 'config.yml'), config);
235
+ console.log(chalk.green(` ✓ Config written: ${domain} → localhost:${port}`));
236
+
237
+ // Route DNS
238
+ try {
239
+ execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });
240
+ console.log(chalk.green(` ✓ DNS CNAME created: ${domain}`));
241
+ } catch (e: any) {
242
+ if (e.message?.includes('already exists') || e.stderr?.includes('already exists')) {
243
+ console.log(chalk.green(` ✓ DNS CNAME already exists for ${domain}`));
244
+ } else {
245
+ console.log(chalk.yellow(` ⚠ DNS routing failed — add CNAME manually: ${domain} → ${tunnelId}.cfargotunnel.com`));
246
+ }
247
+ }
248
+
249
+ // Start with PM2
250
+ let started = false;
251
+ try {
252
+ execSync('which pm2', { timeout: 3000 });
253
+ try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
254
+ execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });
255
+ try { execSync('pm2 save 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
256
+ console.log(chalk.green(' ✓ Tunnel running via PM2 (auto-restarts on crash)'));
257
+ started = true;
258
+ } catch { /* PM2 not available */ }
259
+
260
+ if (!started) {
261
+ // Try npm install pm2 globally, then retry
262
+ try {
263
+ console.log(chalk.dim(' Installing PM2 for process management...'));
264
+ execSync('npm install -g pm2', { timeout: 60000, stdio: 'pipe' });
265
+ try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
266
+ execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });
267
+ try { execSync('pm2 save 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
268
+ console.log(chalk.green(' ✓ PM2 installed + tunnel running (auto-restarts on crash)'));
269
+ started = true;
270
+ } catch {
271
+ console.log(chalk.yellow(' ⚠ PM2 not available — tunnel started in background'));
272
+ console.log(chalk.dim(' Install PM2 for auto-restart: npm install -g pm2'));
273
+ try {
274
+ const { spawn } = await import('child_process');
275
+ const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });
276
+ child.unref();
277
+ started = true;
278
+ } catch { /* best effort */ }
279
+ }
280
+ }
281
+
282
+ console.log('');
283
+ console.log(chalk.green.bold(` ✓ Tunnel deployed! Your dashboard will be at https://${domain}`));
284
+ console.log('');
285
+
286
+ return { tunnelId, domain, port, tunnelName };
287
+ }
288
+
289
+ // ─── Install cloudflared binary ─────────────────────
290
+
291
+ async function installCloudflared(): Promise<void> {
292
+ const plat = platform();
293
+ const a = arch();
294
+
295
+ if (plat === 'darwin') {
296
+ try {
297
+ execSync('which brew', { timeout: 3000 });
298
+ execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });
299
+ return;
300
+ } catch { /* no brew, direct download */ }
301
+ const cfArch = a === 'arm64' ? 'arm64' : 'amd64';
302
+ execSync(
303
+ `curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch} && chmod +x /usr/local/bin/cloudflared`,
304
+ { timeout: 60000 },
305
+ );
306
+ } else if (plat === 'linux') {
307
+ const cfArch = a === 'arm64' ? 'arm64' : 'amd64';
308
+ execSync(
309
+ `curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch} && chmod +x /usr/local/bin/cloudflared`,
310
+ { timeout: 60000 },
311
+ );
312
+ } else {
313
+ throw new Error('Unsupported platform: ' + plat);
314
+ }
315
+ }
@@ -125,18 +125,25 @@ export async function runSetupWizard(): Promise<void> {
125
125
  const database = await promptDatabase(inquirer, chalk);
126
126
 
127
127
  // ─── Step 3: Deployment ──────────────────────────
128
- const { target: deployTarget } = await promptDeployment(inquirer, chalk);
128
+ const deploymentResult = await promptDeployment(inquirer, chalk);
129
+ const deployTarget = deploymentResult.target;
129
130
 
130
131
  // ─── Step 4: Custom Domain ───────────────────────
131
- const domain = await promptDomain(inquirer, chalk, deployTarget);
132
+ // Skip if Cloudflare Tunnel — domain was already configured during tunnel setup
133
+ const domain = deploymentResult.tunnel
134
+ ? { customDomain: deploymentResult.tunnel.domain }
135
+ : await promptDomain(inquirer, chalk, deployTarget);
132
136
 
133
137
  // ─── Step 5: Domain Registration ─────────────────
134
- const registration = await promptRegistration(
135
- inquirer, chalk, ora,
136
- domain.customDomain,
137
- company.companyName,
138
- company.adminEmail,
139
- );
138
+ // Skip for tunnel — DNS is already configured by cloudflared
139
+ const registration = deploymentResult.tunnel
140
+ ? { registered: true, verificationStatus: 'verified' as const } as any
141
+ : await promptRegistration(
142
+ inquirer, chalk, ora,
143
+ domain.customDomain,
144
+ company.companyName,
145
+ company.adminEmail,
146
+ );
140
147
 
141
148
  // ─── Install DB driver if needed ───────────────
142
149
  await ensureDbDriver(database.type, ora, chalk);
@@ -147,7 +154,7 @@ export async function runSetupWizard(): Promise<void> {
147
154
  console.log('');
148
155
 
149
156
  const result = await provision(
150
- { company, database, deployTarget, domain, registration },
157
+ { company, database, deployTarget, domain, registration, tunnel: deploymentResult.tunnel },
151
158
  ora,
152
159
  chalk,
153
160
  );
@@ -29,6 +29,12 @@ export interface ProvisionConfig {
29
29
  deployTarget: DeployTarget;
30
30
  domain: DomainSelection;
31
31
  registration?: RegistrationSelection;
32
+ tunnel?: {
33
+ tunnelId: string;
34
+ domain: string;
35
+ port: number;
36
+ tunnelName: string;
37
+ };
32
38
  }
33
39
 
34
40
  export interface ProvisionResult {
@@ -127,12 +133,24 @@ export async function provision(
127
133
 
128
134
  // ─── Admin Account ─────────────────────────────
129
135
  spinner.start('Creating admin account...');
130
- const admin = await db.createUser({
131
- email: config.company.adminEmail,
132
- name: 'Admin',
133
- role: 'owner',
134
- password: config.company.adminPassword,
135
- });
136
+ let admin: any;
137
+ try {
138
+ admin = await db.createUser({
139
+ email: config.company.adminEmail,
140
+ name: 'Admin',
141
+ role: 'owner',
142
+ password: config.company.adminPassword,
143
+ });
144
+ } catch (err: any) {
145
+ // If the user already exists (re-install), look them up instead
146
+ if (err.message?.includes('duplicate key') || err.message?.includes('UNIQUE constraint') || err.code === '23505') {
147
+ admin = await db.getUserByEmail(config.company.adminEmail);
148
+ if (!admin) throw err; // genuinely broken
149
+ spinner.text = 'Admin account already exists, reusing...';
150
+ } else {
151
+ throw err;
152
+ }
153
+ }
136
154
  await db.logEvent({
137
155
  actor: admin.id,
138
156
  actorType: 'system',
@@ -182,7 +200,30 @@ async function deploy(
182
200
  spinner: any,
183
201
  chalk: any,
184
202
  ): Promise<DeployResult> {
185
- const { deployTarget, company, database, domain } = config;
203
+ const { deployTarget, company, database, domain, tunnel } = config;
204
+
205
+ // ── Cloudflare Tunnel ─────────────────────────────
206
+ if (deployTarget === 'cloudflare-tunnel' && tunnel) {
207
+ spinner.start(`Starting local server on port ${tunnel.port}...`);
208
+ const { createServer } = await import('../server.js');
209
+ const server = createServer({ port: tunnel.port, db, jwtSecret });
210
+ const handle = await server.start();
211
+ spinner.succeed('Server running');
212
+
213
+ console.log('');
214
+ console.log(chalk.green.bold(' AgenticMail Enterprise is live!'));
215
+ console.log('');
216
+ console.log(` ${chalk.bold('Public URL:')} ${chalk.cyan('https://' + tunnel.domain)}`);
217
+ console.log(` ${chalk.bold('Local:')} ${chalk.cyan('http://localhost:' + tunnel.port)}`);
218
+ console.log(` ${chalk.bold('Tunnel:')} ${tunnel.tunnelName} (${tunnel.tunnelId})`);
219
+ console.log(` ${chalk.bold('Admin:')} ${company.adminEmail}`);
220
+ console.log('');
221
+ console.log(chalk.dim(' Tunnel is managed by PM2 — auto-restarts on crash.'));
222
+ console.log(chalk.dim(' Manage: pm2 status | pm2 logs cloudflared | pm2 restart cloudflared'));
223
+ console.log(chalk.dim(' Press Ctrl+C to stop the server'));
224
+
225
+ return { url: 'https://' + tunnel.domain, close: handle.close };
226
+ }
186
227
 
187
228
  // ── Cloud ─────────────────────────────────────────
188
229
  if (deployTarget === 'cloud') {