@realtimex/email-automator 2.26.0 → 2.28.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.
package/api/server.ts CHANGED
@@ -21,9 +21,15 @@ import { setEncryptionKey, getEncryptionKeyHex } from './src/utils/encryption.js
21
21
  async function initializePersistenceEncryption() {
22
22
  try {
23
23
  const supabase = getServiceRoleSupabase();
24
+
24
25
  if (!supabase) {
25
- logger.info('Supabase not configured yet, skipping encryption initialization');
26
- logger.info('IMAP features will be available after completing Setup Wizard');
26
+ // BYOK mode: Supabase not configured at startup (credentials come via HTTP headers)
27
+ // Generate encryption key anyway and keep it in memory
28
+ logger.info('Supabase not configured yet (BYOK mode)');
29
+ logger.info('Generating encryption key in memory - will persist when Supabase becomes available');
30
+ const newKey = crypto.randomBytes(32).toString('hex');
31
+ setEncryptionKey(newKey);
32
+ logger.info('✓ Encryption key generated and loaded in memory');
27
33
  return;
28
34
  }
29
35
 
@@ -37,6 +43,10 @@ async function initializePersistenceEncryption() {
37
43
 
38
44
  if (error) {
39
45
  logger.warn('Failed to query user_settings for encryption key', { error });
46
+ // Still generate a key for in-memory use
47
+ const newKey = crypto.randomBytes(32).toString('hex');
48
+ setEncryptionKey(newKey);
49
+ logger.info('✓ Generated fallback encryption key due to DB error');
40
50
  return;
41
51
  }
42
52
 
@@ -74,12 +84,17 @@ async function initializePersistenceEncryption() {
74
84
  logger.info('✓ Encryption key saved to database');
75
85
  } else {
76
86
  logger.info('No users found yet, encryption key loaded in memory');
77
- logger.info('Key will be persisted when first user is created');
87
+ logger.info('Key will be persisted when users are created');
78
88
  }
79
89
 
80
90
  } catch (err) {
81
91
  logger.error('Error initializing encryption:', err);
82
- logger.warn('⚠ IMAP features may not work properly');
92
+ // Always ensure we have a key, even if there was an error
93
+ if (!getEncryptionKeyHex()) {
94
+ logger.warn('Generating emergency fallback encryption key');
95
+ const fallbackKey = crypto.randomBytes(32).toString('hex');
96
+ setEncryptionKey(fallbackKey);
97
+ }
83
98
  }
84
99
  }
85
100
 
@@ -7,7 +7,7 @@ import { getGmailService } from '../services/gmail.js';
7
7
  import { getMicrosoftService } from '../services/microsoft.js';
8
8
  import { getImapService } from '../services/imap-service.js';
9
9
  import { createLogger } from '../utils/logger.js';
10
- import { getEncryptionKeyHex } from '../utils/encryption.js';
10
+ import { getEncryptionKeyHex, setEncryptionKey } from '../utils/encryption.js';
11
11
 
12
12
  const router = Router();
13
13
  const logger = createLogger('AuthRoutes');
@@ -24,25 +24,52 @@ router.post('/imap/connect',
24
24
  smtpHost, smtpPort, smtpSecure
25
25
  } = req.body;
26
26
 
27
- // CRITICAL FIX: Ensure user has encryption key before IMAP operations
28
- // This handles the "first user connects IMAP immediately after signup" case
29
- const currentKey = getEncryptionKeyHex();
30
- if (currentKey) {
31
- const { data: userSettings } = await req.supabase!
27
+ // CRITICAL FIX: Sync encryption key between server memory and database
28
+ // In BYOK mode, database trigger generates key but server has random key in memory
29
+ // We need to sync them: prefer database key (persistent) over server key (ephemeral)
30
+ const { data: userSettings, error: fetchError } = await req.supabase!
31
+ .from('user_settings')
32
+ .select('encryption_key')
33
+ .eq('user_id', req.user!.id)
34
+ .single();
35
+
36
+ if (fetchError) {
37
+ logger.error('Failed to fetch user settings', { error: fetchError });
38
+ throw new ValidationError('Failed to load user settings. Please try again.');
39
+ }
40
+
41
+ const currentServerKey = getEncryptionKeyHex();
42
+ const databaseKey = userSettings?.encryption_key;
43
+
44
+ logger.info('Encryption key sync check', {
45
+ hasServerKey: !!currentServerKey,
46
+ hasDatabaseKey: !!databaseKey,
47
+ userId: req.user!.id
48
+ });
49
+
50
+ if (databaseKey) {
51
+ // Database has a key - use it (it's the source of truth)
52
+ if (currentServerKey !== databaseKey) {
53
+ logger.info('Loading encryption key from database (overriding server memory)');
54
+ setEncryptionKey(databaseKey);
55
+ }
56
+ } else if (currentServerKey) {
57
+ // Server has key but database doesn't - persist to database
58
+ logger.info('Database missing encryption key, persisting server key');
59
+ const { error: updateError } = await req.supabase!
32
60
  .from('user_settings')
33
- .select('encryption_key')
34
- .eq('user_id', req.user!.id)
35
- .single();
36
-
37
- if (userSettings && !userSettings.encryption_key) {
38
- // User doesn't have encryption key yet - persist it now
39
- logger.info('User missing encryption key, persisting immediately', { userId: req.user!.id });
40
- await req.supabase!
41
- .from('user_settings')
42
- .update({ encryption_key: currentKey })
43
- .eq('user_id', req.user!.id);
44
- logger.info('✓ Encryption key persisted to user');
61
+ .update({ encryption_key: currentServerKey })
62
+ .eq('user_id', req.user!.id);
63
+
64
+ if (updateError) {
65
+ logger.error('Failed to persist encryption key', { error: updateError });
66
+ throw new ValidationError('Failed to initialize encryption. Please try again.');
45
67
  }
68
+ logger.info('✓ Encryption key persisted to database');
69
+ } else {
70
+ // Neither server nor database has key - this should never happen with new trigger
71
+ logger.error('No encryption key found in server OR database!');
72
+ throw new ValidationError('Encryption not initialized. Please contact support.');
46
73
  }
47
74
 
48
75
  const imapService = getImapService();
@@ -20,8 +20,13 @@ async function initializePersistenceEncryption() {
20
20
  try {
21
21
  const supabase = getServiceRoleSupabase();
22
22
  if (!supabase) {
23
- logger.info('Supabase not configured yet, skipping encryption initialization');
24
- logger.info('IMAP features will be available after completing Setup Wizard');
23
+ // BYOK mode: Supabase not configured at startup (credentials come via HTTP headers)
24
+ // Generate encryption key anyway and keep it in memory
25
+ logger.info('Supabase not configured yet (BYOK mode)');
26
+ logger.info('Generating encryption key in memory - will persist when Supabase becomes available');
27
+ const newKey = crypto.randomBytes(32).toString('hex');
28
+ setEncryptionKey(newKey);
29
+ logger.info('✓ Encryption key generated and loaded in memory');
25
30
  return;
26
31
  }
27
32
  // 1. Check if ANY user has an encryption key stored
@@ -33,6 +38,10 @@ async function initializePersistenceEncryption() {
33
38
  .limit(1);
34
39
  if (error) {
35
40
  logger.warn('Failed to query user_settings for encryption key', { error });
41
+ // Still generate a key for in-memory use
42
+ const newKey = crypto.randomBytes(32).toString('hex');
43
+ setEncryptionKey(newKey);
44
+ logger.info('✓ Generated fallback encryption key due to DB error');
36
45
  return;
37
46
  }
38
47
  if (users && users.length > 0 && users[0].encryption_key) {
@@ -63,12 +72,17 @@ async function initializePersistenceEncryption() {
63
72
  }
64
73
  else {
65
74
  logger.info('No users found yet, encryption key loaded in memory');
66
- logger.info('Key will be persisted when first user is created');
75
+ logger.info('Key will be persisted when users are created');
67
76
  }
68
77
  }
69
78
  catch (err) {
70
79
  logger.error('Error initializing encryption:', err);
71
- logger.warn('⚠ IMAP features may not work properly');
80
+ // Always ensure we have a key, even if there was an error
81
+ if (!getEncryptionKeyHex()) {
82
+ logger.warn('Generating emergency fallback encryption key');
83
+ const fallbackKey = crypto.randomBytes(32).toString('hex');
84
+ setEncryptionKey(fallbackKey);
85
+ }
72
86
  }
73
87
  }
74
88
  // Periodic sync: Persist in-memory encryption key to users without one
@@ -7,31 +7,56 @@ import { getGmailService } from '../services/gmail.js';
7
7
  import { getMicrosoftService } from '../services/microsoft.js';
8
8
  import { getImapService } from '../services/imap-service.js';
9
9
  import { createLogger } from '../utils/logger.js';
10
- import { getEncryptionKeyHex } from '../utils/encryption.js';
10
+ import { getEncryptionKeyHex, setEncryptionKey } from '../utils/encryption.js';
11
11
  const router = Router();
12
12
  const logger = createLogger('AuthRoutes');
13
13
  // IMAP/SMTP Connection
14
14
  router.post('/imap/connect', connectionRateLimit, // More lenient for connection testing (30 attempts / 15 min)
15
15
  authMiddleware, validateBody(schemas.imapConnect), asyncHandler(async (req, res) => {
16
16
  const { email, password, imapHost, imapPort, imapSecure, smtpHost, smtpPort, smtpSecure } = req.body;
17
- // CRITICAL FIX: Ensure user has encryption key before IMAP operations
18
- // This handles the "first user connects IMAP immediately after signup" case
19
- const currentKey = getEncryptionKeyHex();
20
- if (currentKey) {
21
- const { data: userSettings } = await req.supabase
17
+ // CRITICAL FIX: Sync encryption key between server memory and database
18
+ // In BYOK mode, database trigger generates key but server has random key in memory
19
+ // We need to sync them: prefer database key (persistent) over server key (ephemeral)
20
+ const { data: userSettings, error: fetchError } = await req.supabase
21
+ .from('user_settings')
22
+ .select('encryption_key')
23
+ .eq('user_id', req.user.id)
24
+ .single();
25
+ if (fetchError) {
26
+ logger.error('Failed to fetch user settings', { error: fetchError });
27
+ throw new ValidationError('Failed to load user settings. Please try again.');
28
+ }
29
+ const currentServerKey = getEncryptionKeyHex();
30
+ const databaseKey = userSettings?.encryption_key;
31
+ logger.info('Encryption key sync check', {
32
+ hasServerKey: !!currentServerKey,
33
+ hasDatabaseKey: !!databaseKey,
34
+ userId: req.user.id
35
+ });
36
+ if (databaseKey) {
37
+ // Database has a key - use it (it's the source of truth)
38
+ if (currentServerKey !== databaseKey) {
39
+ logger.info('Loading encryption key from database (overriding server memory)');
40
+ setEncryptionKey(databaseKey);
41
+ }
42
+ }
43
+ else if (currentServerKey) {
44
+ // Server has key but database doesn't - persist to database
45
+ logger.info('Database missing encryption key, persisting server key');
46
+ const { error: updateError } = await req.supabase
22
47
  .from('user_settings')
23
- .select('encryption_key')
24
- .eq('user_id', req.user.id)
25
- .single();
26
- if (userSettings && !userSettings.encryption_key) {
27
- // User doesn't have encryption key yet - persist it now
28
- logger.info('User missing encryption key, persisting immediately', { userId: req.user.id });
29
- await req.supabase
30
- .from('user_settings')
31
- .update({ encryption_key: currentKey })
32
- .eq('user_id', req.user.id);
33
- logger.info('✓ Encryption key persisted to user');
48
+ .update({ encryption_key: currentServerKey })
49
+ .eq('user_id', req.user.id);
50
+ if (updateError) {
51
+ logger.error('Failed to persist encryption key', { error: updateError });
52
+ throw new ValidationError('Failed to initialize encryption. Please try again.');
34
53
  }
54
+ logger.info('✓ Encryption key persisted to database');
55
+ }
56
+ else {
57
+ // Neither server nor database has key - this should never happen with new trigger
58
+ logger.error('No encryption key found in server OR database!');
59
+ throw new ValidationError('Encryption not initialized. Please contact support.');
35
60
  }
36
61
  const imapService = getImapService();
37
62
  const imapConfig = {