@agenticmail/enterprise 0.5.287 → 0.5.289

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/src/cli-serve.ts CHANGED
@@ -140,4 +140,38 @@ export async function runServe(_args: string[]) {
140
140
 
141
141
  await server.start();
142
142
  console.log(`AgenticMail Enterprise server running on :${PORT}`);
143
+
144
+ // Auto-start cloudflared if tunnel token is present
145
+ const tunnelToken = process.env.CLOUDFLARED_TOKEN;
146
+ if (tunnelToken) {
147
+ try {
148
+ const { execSync, spawn } = await import('child_process');
149
+ try {
150
+ execSync('which cloudflared', { timeout: 3000 });
151
+ } catch {
152
+ console.log('[startup] cloudflared not found — skipping tunnel auto-start');
153
+ console.log('[startup] Install cloudflared to enable tunnel: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
154
+ return;
155
+ }
156
+
157
+ // Check if already running
158
+ try {
159
+ execSync('pgrep -f "cloudflared.*tunnel.*run"', { timeout: 3000 });
160
+ console.log('[startup] cloudflared tunnel already running');
161
+ return;
162
+ } catch { /* not running, start it */ }
163
+
164
+ const subdomain = process.env.AGENTICMAIL_SUBDOMAIN || process.env.AGENTICMAIL_DOMAIN || '';
165
+ console.log(`[startup] Starting cloudflared tunnel${subdomain ? ` for ${subdomain}.agenticmail.io` : ''}...`);
166
+
167
+ const child = spawn('cloudflared', ['tunnel', '--no-autoupdate', 'run', '--token', tunnelToken], {
168
+ detached: true,
169
+ stdio: 'ignore',
170
+ });
171
+ child.unref();
172
+ console.log('[startup] cloudflared tunnel started (pid ' + child.pid + ')');
173
+ } catch (e: any) {
174
+ console.warn('[startup] Could not auto-start cloudflared: ' + e.message);
175
+ }
176
+ }
143
177
  }
@@ -37,6 +37,28 @@ export async function runRecover(args: string[]): Promise<void> {
37
37
  const { default: chalk } = await import('chalk');
38
38
  const { default: ora } = await import('ora');
39
39
 
40
+ // ─── Detect recovery type ─────────────────────────
41
+ const isCloud = hasFlag(args, '--cloud') || hasFlag(args, '-c');
42
+ if (isCloud || (!getFlag(args, '--domain') && !getFlag(args, '--key'))) {
43
+ // Ask which type
44
+ if (!isCloud && !getFlag(args, '--domain')) {
45
+ const { recoveryType } = await inquirer.prompt([{
46
+ type: 'list',
47
+ name: 'recoveryType',
48
+ message: 'What are you recovering?',
49
+ choices: [
50
+ { name: `AgenticMail Cloud ${chalk.dim('(yourname.agenticmail.io)')}`, value: 'cloud' },
51
+ { name: `Custom Domain ${chalk.dim('(your own domain)')}`, value: 'domain' },
52
+ ],
53
+ }]);
54
+ if (recoveryType === 'cloud') {
55
+ return runCloudRecover(args, inquirer, chalk, ora);
56
+ }
57
+ } else if (isCloud) {
58
+ return runCloudRecover(args, inquirer, chalk, ora);
59
+ }
60
+ }
61
+
40
62
  console.log('');
41
63
  console.log(chalk.bold(' AgenticMail Enterprise — Domain Recovery'));
42
64
  console.log(chalk.dim(' Recover your domain registration on a new machine.'));
@@ -276,6 +298,217 @@ export async function runRecover(args: string[]): Promise<void> {
276
298
  }
277
299
  }
278
300
 
301
+ // ─── AgenticMail Cloud Recovery ─────────────────────
302
+
303
+ const REGISTRY_URL = process.env.AGENTICMAIL_SUBDOMAIN_REGISTRY_URL
304
+ || 'https://registry.agenticmail.io';
305
+
306
+ async function runCloudRecover(args: string[], inquirer: any, chalk: any, ora: any): Promise<void> {
307
+ console.log('');
308
+ console.log(chalk.bold(' AgenticMail Cloud — Recovery'));
309
+ console.log(chalk.dim(' Recover your agenticmail.io subdomain on a new machine.'));
310
+ console.log('');
311
+ console.log(chalk.dim(' You will need your AGENTICMAIL_VAULT_KEY — the key from your'));
312
+ console.log(chalk.dim(' original installation\'s ~/.agenticmail/.env file.'));
313
+ console.log('');
314
+
315
+ // Step 1: Get vault key
316
+ let vaultKey = getFlag(args, '--vault-key') || process.env.AGENTICMAIL_VAULT_KEY;
317
+ if (!vaultKey) {
318
+ const answer = await inquirer.prompt([{
319
+ type: 'password',
320
+ name: 'vaultKey',
321
+ message: 'Your AGENTICMAIL_VAULT_KEY:',
322
+ mask: '*',
323
+ validate: (v: string) => v.trim().length >= 16 ? true : 'Key seems too short — check your backup',
324
+ }]);
325
+ vaultKey = answer.vaultKey.trim();
326
+ }
327
+
328
+ // Step 2: Optionally get subdomain name (speeds up recovery)
329
+ let subdomain = getFlag(args, '--name') || getFlag(args, '--subdomain');
330
+ if (!subdomain) {
331
+ const answer = await inquirer.prompt([{
332
+ type: 'input',
333
+ name: 'subdomain',
334
+ message: 'Your subdomain (optional — press Enter to auto-detect):',
335
+ suffix: chalk.dim('.agenticmail.io'),
336
+ }]);
337
+ subdomain = answer.subdomain?.trim() || undefined;
338
+ }
339
+
340
+ // Step 3: Recover from registry
341
+ const { createHash } = await import('crypto');
342
+ const vaultKeyHash = createHash('sha256').update(vaultKey!).digest('hex');
343
+
344
+ const spinner = ora('Recovering subdomain credentials...').start();
345
+ try {
346
+ const body: any = { vaultKeyHash };
347
+ if (subdomain) body.name = subdomain;
348
+
349
+ const resp = await fetch(`${REGISTRY_URL}/recover`, {
350
+ method: 'POST',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify(body),
353
+ });
354
+ const data = await resp.json() as any;
355
+
356
+ if (!data.success) {
357
+ spinner.fail(data.error || 'Recovery failed');
358
+ console.log('');
359
+ console.log(chalk.dim(' Make sure you are using the exact AGENTICMAIL_VAULT_KEY from'));
360
+ console.log(chalk.dim(' your original installation (~/.agenticmail/.env).'));
361
+ return;
362
+ }
363
+
364
+ spinner.succeed(`Recovered: ${chalk.bold(data.fqdn)}`);
365
+
366
+ // Step 4: Save to .env
367
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('fs');
368
+ const { join } = await import('path');
369
+ const { homedir } = await import('os');
370
+
371
+ const amDir = join(homedir(), '.agenticmail');
372
+ mkdirSync(amDir, { recursive: true });
373
+ const envPath = join(amDir, '.env');
374
+
375
+ let envContent = '';
376
+ if (existsSync(envPath)) {
377
+ envContent = readFileSync(envPath, 'utf8');
378
+ }
379
+
380
+ // Update or add each key
381
+ const updates: Record<string, string> = {
382
+ AGENTICMAIL_VAULT_KEY: vaultKey!,
383
+ AGENTICMAIL_SUBDOMAIN: data.subdomain,
384
+ AGENTICMAIL_DOMAIN: data.fqdn,
385
+ CLOUDFLARED_TOKEN: data.tunnelToken,
386
+ };
387
+
388
+ for (const [key, val] of Object.entries(updates)) {
389
+ if (!val) continue;
390
+ const regex = new RegExp(`^${key}=.*$`, 'm');
391
+ if (regex.test(envContent)) {
392
+ envContent = envContent.replace(regex, `${key}=${val}`);
393
+ } else {
394
+ envContent += `${envContent.endsWith('\n') ? '' : '\n'}${key}=${val}\n`;
395
+ }
396
+ }
397
+
398
+ writeFileSync(envPath, envContent, { mode: 0o600 });
399
+
400
+ console.log('');
401
+ console.log(chalk.green.bold(' Recovery complete!'));
402
+ console.log('');
403
+ console.log(` Subdomain: ${chalk.bold(data.fqdn)}`);
404
+ console.log(` Tunnel Token: ${chalk.dim('saved to ~/.agenticmail/.env')}`);
405
+ console.log(` Vault Key: ${chalk.dim('saved to ~/.agenticmail/.env')}`);
406
+ console.log('');
407
+ console.log(chalk.bold(' Next steps:'));
408
+ console.log('');
409
+
410
+ // Ask for DATABASE_URL right now
411
+ const envFilePath = join(amDir, '.env');
412
+ let currentEnv = '';
413
+ try { currentEnv = readFileSync(envFilePath, 'utf8'); } catch {}
414
+ const hasDbUrl = currentEnv.includes('DATABASE_URL=') && !currentEnv.includes('DATABASE_URL=\n');
415
+
416
+ if (!hasDbUrl) {
417
+ console.log(chalk.yellow(' Your database connection string is needed to restore your data.'));
418
+ console.log(chalk.dim(' This is the same DATABASE_URL from your original installation.'));
419
+ console.log(chalk.dim(' Example: postgresql://user:pass@host:5432/dbname\n'));
420
+
421
+ const { dbUrl } = await inquirer.prompt([{
422
+ type: 'input',
423
+ name: 'dbUrl',
424
+ message: 'DATABASE_URL:',
425
+ validate: (v: string) => v.trim().length > 5 ? true : 'Enter your database connection string',
426
+ }]);
427
+
428
+ const regex = /^DATABASE_URL=.*$/m;
429
+ if (regex.test(currentEnv)) {
430
+ currentEnv = currentEnv.replace(regex, `DATABASE_URL=${dbUrl.trim()}`);
431
+ } else {
432
+ currentEnv += `${currentEnv.endsWith('\n') ? '' : '\n'}DATABASE_URL=${dbUrl.trim()}\n`;
433
+ }
434
+ writeFileSync(envFilePath, currentEnv, { mode: 0o600 });
435
+ console.log(chalk.green(' DATABASE_URL saved'));
436
+ }
437
+
438
+ // Check if JWT_SECRET is present — if not, warn them
439
+ if (!currentEnv.includes('JWT_SECRET=') || currentEnv.includes('JWT_SECRET=\n')) {
440
+ console.log('');
441
+ console.log(chalk.yellow(' Note: JWT_SECRET is missing — a new one will be generated on start.'));
442
+ console.log(chalk.dim(' This means existing login sessions from the old machine won\'t work.'));
443
+ console.log(chalk.dim(' Users will need to log in again. This is normal for recovery.'));
444
+ }
445
+
446
+ console.log('');
447
+ console.log(` Start your instance:`);
448
+ console.log(` ${chalk.cyan('npx @agenticmail/enterprise start')}`);
449
+ console.log('');
450
+ console.log(chalk.dim(' The server will auto-start cloudflared with your tunnel token.'));
451
+ console.log(chalk.dim(' Your dashboard will be live again at https://' + data.fqdn));
452
+ console.log('');
453
+
454
+ // Step 5: Offer to install cloudflared now
455
+ const { doInstall } = await inquirer.prompt([{
456
+ type: 'confirm',
457
+ name: 'doInstall',
458
+ message: 'Install cloudflared and start the tunnel now?',
459
+ default: true,
460
+ }]);
461
+
462
+ if (doInstall) {
463
+ const { execSync } = await import('child_process');
464
+ const { platform, arch } = await import('os');
465
+
466
+ // Install cloudflared if needed
467
+ try {
468
+ execSync('which cloudflared', { timeout: 3000 });
469
+ console.log(chalk.green(' cloudflared already installed'));
470
+ } catch {
471
+ const spinner2 = ora('Installing cloudflared...').start();
472
+ try {
473
+ const os = platform();
474
+ if (os === 'darwin') {
475
+ try {
476
+ execSync('brew install cloudflared', { stdio: 'pipe', timeout: 120000 });
477
+ } catch {
478
+ const cfArch = arch() === 'arm64' ? 'arm64' : 'amd64';
479
+ execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
480
+ }
481
+ } else {
482
+ const cfArch = arch() === 'arm64' ? 'arm64' : 'amd64';
483
+ execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
484
+ }
485
+ spinner2.succeed('cloudflared installed');
486
+ } catch (e: any) {
487
+ spinner2.fail('Could not install cloudflared: ' + e.message);
488
+ console.log(chalk.dim(' Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
489
+ }
490
+ }
491
+
492
+ // Start via PM2
493
+ try {
494
+ const { execSync: ex } = await import('child_process');
495
+ ex('which pm2', { timeout: 3000 });
496
+ try { ex('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch {}
497
+ ex(`pm2 start cloudflared --name cloudflared -- tunnel --no-autoupdate run --token ${data.tunnelToken}`, { timeout: 15000 });
498
+ try { ex('pm2 save 2>/dev/null', { timeout: 5000 }); } catch {}
499
+ console.log(chalk.green(' Tunnel running via PM2'));
500
+ } catch {
501
+ console.log(chalk.dim(' Start the tunnel manually:'));
502
+ console.log(chalk.cyan(` cloudflared tunnel --no-autoupdate run --token ${data.tunnelToken}`));
503
+ }
504
+ }
505
+
506
+ } catch (err: any) {
507
+ spinner.fail('Recovery failed: ' + err.message);
508
+ console.log(chalk.dim(' Check your internet connection and try again.'));
509
+ }
510
+ }
511
+
279
512
  /** Detect DB type from connection string */
280
513
  function detectDbType(url: string): string {
281
514
  const u = url.toLowerCase().trim();
@@ -17,7 +17,7 @@ const execP = promisify(execCb);
17
17
  export type DeployTarget = 'cloud' | 'cloudflare-tunnel' | 'fly' | 'railway' | 'docker' | 'local';
18
18
 
19
19
  const SUBDOMAIN_REGISTRY_URL = process.env.AGENTICMAIL_SUBDOMAIN_REGISTRY_URL
20
- || 'https://subdomain-registry.agenticmail.io';
20
+ || 'https://registry.agenticmail.io';
21
21
 
22
22
  export interface DeploymentSelection {
23
23
  target: DeployTarget;
@@ -52,11 +52,11 @@ export async function promptDeployment(
52
52
  message: 'Deploy to:',
53
53
  choices: [
54
54
  {
55
- name: `AgenticMail Cloud ${chalk.dim('(managed, instant URL)')}`,
55
+ name: `AgenticMail Cloud ${chalk.green('← recommended')} ${chalk.dim('(instant URL, zero config)')}`,
56
56
  value: 'cloud',
57
57
  },
58
58
  {
59
- name: `Cloudflare Tunnel ${chalk.green('← recommended')} ${chalk.dim('(self-hosted, free, no ports)')}`,
59
+ name: `Cloudflare Tunnel ${chalk.dim('(self-hosted, free, no ports)')}`,
60
60
  value: 'cloudflare-tunnel',
61
61
  },
62
62
  {
@@ -253,6 +253,25 @@ async function runCloudSetup(
253
253
  console.log(chalk.bold.green(' ✓ Setup Complete!'));
254
254
  console.log('');
255
255
  console.log(` Your dashboard: ${chalk.bold.cyan('https://' + fqdn)}`);
256
+
257
+ // ── CRITICAL: Vault key backup warning ─────────
258
+ console.log('');
259
+ console.log(chalk.bgYellow.black.bold(' ⚠ BACK UP ~/.agenticmail/.env ⚠ '));
260
+ console.log('');
261
+ console.log(chalk.yellow(' This file contains EVERYTHING needed to recover if your machine crashes:'));
262
+ console.log('');
263
+ console.log(chalk.yellow(' DATABASE_URL — your database connection'));
264
+ console.log(chalk.yellow(' JWT_SECRET — login session signing key'));
265
+ console.log(chalk.yellow(' AGENTICMAIL_VAULT_KEY — encrypted credentials + subdomain recovery'));
266
+ console.log(chalk.yellow(' CLOUDFLARED_TOKEN — tunnel connection token'));
267
+ console.log(chalk.yellow(' PORT — server port'));
268
+ console.log('');
269
+ console.log(chalk.bold.yellow(' Copy this file to a safe place (password manager, cloud drive, etc.)'));
270
+ console.log(chalk.bold.yellow(' Without it, you CANNOT recover your subdomain or encrypted data.'));
271
+ console.log('');
272
+ console.log(chalk.dim(' To recover on a new machine:'));
273
+ console.log(chalk.cyan(' npx @agenticmail/enterprise recover --cloud'));
274
+ console.log('');
256
275
  console.log('');
257
276
  console.log(chalk.dim(' To start your instance, run these two processes:'));
258
277
  console.log('');
@@ -182,12 +182,37 @@ export async function provision(
182
182
  || (config.deployTarget === 'local' ? 3000 : undefined)
183
183
  || (config.deployTarget === 'docker' ? 3000 : undefined)
184
184
  || 3200;
185
+ // Read existing .env to preserve cloud/tunnel values
186
+ let existingEnv = '';
187
+ const envFilePath = join(envDir, '.env');
188
+ if (existsSync(envFilePath)) {
189
+ try { existingEnv = (await import('fs')).readFileSync(envFilePath, 'utf8'); } catch {}
190
+ }
191
+
192
+ // Build key=value map preserving existing keys we don't set here
193
+ const envMap = new Map<string, string>();
194
+ for (const line of existingEnv.split('\n')) {
195
+ const t = line.trim();
196
+ if (!t || t.startsWith('#')) continue;
197
+ const eq = t.indexOf('=');
198
+ if (eq > 0) envMap.set(t.slice(0, eq).trim(), t.slice(eq + 1).trim());
199
+ }
200
+
201
+ // Set/overwrite the keys from this setup step
202
+ envMap.set('DATABASE_URL', config.database.connectionString || '');
203
+ envMap.set('JWT_SECRET', jwtSecret);
204
+ envMap.set('AGENTICMAIL_VAULT_KEY', vaultKey);
205
+ envMap.set('PORT', String(port));
206
+
207
+ // Cloud deployment values (from deployment.ts step)
208
+ if (config.cloud?.tunnelToken) envMap.set('CLOUDFLARED_TOKEN', config.cloud.tunnelToken);
209
+ if (config.cloud?.subdomain) envMap.set('AGENTICMAIL_SUBDOMAIN', config.cloud.subdomain);
210
+ if (config.cloud?.fqdn) envMap.set('AGENTICMAIL_DOMAIN', config.cloud.fqdn);
211
+
185
212
  const envContent = [
186
213
  '# AgenticMail Enterprise — auto-generated by setup wizard',
187
- `DATABASE_URL=${config.database.connectionString || ''}`,
188
- `JWT_SECRET=${jwtSecret}`,
189
- `AGENTICMAIL_VAULT_KEY=${vaultKey}`,
190
- `PORT=${port}`,
214
+ '# BACK UP THIS FILE! You need it to recover on a new machine.',
215
+ ...Array.from(envMap.entries()).map(([k, v]) => `${k}=${v}`),
191
216
  ].join('\n') + '\n';
192
217
  writeFileSync(join(envDir, '.env'), envContent, { mode: 0o600 });
193
218
  spinner.succeed(`Config saved to ~/.agenticmail/.env`);