@realtimex/email-automator 2.28.0 → 2.28.2

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
@@ -15,126 +15,7 @@ import { logger } from './src/utils/logger.js';
15
15
  import { getServerSupabase, getServiceRoleSupabase } from './src/services/supabase.js';
16
16
  import { startScheduler, stopScheduler } from './src/services/scheduler.js';
17
17
  import { setEncryptionKey, getEncryptionKeyHex } from './src/utils/encryption.js';
18
-
19
- // Initialize Persistence Encryption
20
- // NOTE: In RealTimeX Desktop sandbox, all config is stored in Supabase (no .env files)
21
- async function initializePersistenceEncryption() {
22
- try {
23
- const supabase = getServiceRoleSupabase();
24
-
25
- if (!supabase) {
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');
33
- return;
34
- }
35
-
36
- // 1. Check if ANY user has an encryption key stored
37
- // In sandbox mode, encryption key is always in database
38
- const { data: users, error } = await supabase
39
- .from('user_settings')
40
- .select('user_id, encryption_key')
41
- .not('encryption_key', 'is', null)
42
- .limit(1);
43
-
44
- if (error) {
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');
50
- return;
51
- }
52
-
53
- if (users && users.length > 0 && users[0].encryption_key) {
54
- // Found a persisted key in database
55
- logger.info('✓ Loaded encryption key from database');
56
- setEncryptionKey(users[0].encryption_key);
57
- return;
58
- }
59
-
60
- // 2. No key in database - auto-generate and persist
61
- // This happens on first run or after fresh database setup
62
- logger.info('No encryption key found in database, generating new key...');
63
- const newKey = crypto.randomBytes(32).toString('hex');
64
- setEncryptionKey(newKey);
65
-
66
- // 3. Persist to all existing users in database
67
- const { data: allUsers } = await supabase
68
- .from('user_settings')
69
- .select('user_id')
70
- .limit(100);
71
-
72
- if (allUsers && allUsers.length > 0) {
73
- logger.info(`Saving encryption key to database for ${allUsers.length} user(s)...`);
74
-
75
- // Update all users with the new key
76
- const updates = allUsers.map(user =>
77
- supabase
78
- .from('user_settings')
79
- .update({ encryption_key: newKey })
80
- .eq('user_id', user.user_id)
81
- );
82
-
83
- await Promise.all(updates);
84
- logger.info('✓ Encryption key saved to database');
85
- } else {
86
- logger.info('No users found yet, encryption key loaded in memory');
87
- logger.info('Key will be persisted when users are created');
88
- }
89
-
90
- } catch (err) {
91
- logger.error('Error initializing encryption:', err);
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
- }
98
- }
99
- }
100
-
101
- // Periodic sync: Persist in-memory encryption key to users without one
102
- // This handles the "first user" case and any users created before key was persisted
103
- async function syncEncryptionKeyToUsers() {
104
- try {
105
- const supabase = getServiceRoleSupabase();
106
- if (!supabase) return;
107
-
108
- const currentKey = getEncryptionKeyHex();
109
- if (!currentKey) return; // No key in memory yet
110
-
111
- // Find users without encryption key
112
- const { data: usersWithoutKey, error } = await supabase
113
- .from('user_settings')
114
- .select('user_id')
115
- .is('encryption_key', null)
116
- .limit(100);
117
-
118
- if (error || !usersWithoutKey || usersWithoutKey.length === 0) {
119
- return; // No users to update
120
- }
121
-
122
- logger.info(`Found ${usersWithoutKey.length} user(s) without encryption key, persisting...`);
123
-
124
- // Update all users without a key
125
- const updates = usersWithoutKey.map(user =>
126
- supabase
127
- .from('user_settings')
128
- .update({ encryption_key: currentKey })
129
- .eq('user_id', user.user_id)
130
- );
131
-
132
- await Promise.all(updates);
133
- logger.info(`✓ Persisted encryption key to ${usersWithoutKey.length} user(s)`);
134
- } catch (err) {
135
- logger.warn('Error syncing encryption key to users:', { error: err });
136
- }
137
- }
18
+ import { initializePersistenceEncryption, syncEncryptionKeyToUsers } from './src/services/encryptionInit.js';
138
19
 
139
20
  const __filename = fileURLToPath(import.meta.url);
140
21
  const __dirname = path.dirname(__filename);
@@ -7,6 +7,7 @@ import { createLogger, Logger } from '../utils/logger.js';
7
7
  const logger = createLogger('AuthMiddleware');
8
8
 
9
9
  import { getServerSupabase, isValidUrl } from '../services/supabase.js';
10
+ import { initializePersistenceEncryption, isEncryptionReady } from '../services/encryptionInit.js';
10
11
 
11
12
  // Extend Express Request to include user
12
13
  declare global {
@@ -82,6 +83,13 @@ export async function authMiddleware(
82
83
  req.supabase = supabase;
83
84
  // Initialize logger persistence for mock user
84
85
  Logger.setPersistence(supabase, req.user.id);
86
+
87
+ // If encryption is not ready, try to initialize it now
88
+ if (!isEncryptionReady()) {
89
+ initializePersistenceEncryption(supabase).catch(err =>
90
+ logger.warn('Failed to initialize encryption in dev mode', { error: err.message })
91
+ );
92
+ }
85
93
  } else {
86
94
  throw new AuthenticationError('Supabase not configured. Please set up Supabase in the app or provide SUPABASE_URL/ANON_KEY in .env');
87
95
  }
@@ -118,6 +126,13 @@ export async function authMiddleware(
118
126
  throw new AuthenticationError('Invalid or expired token');
119
127
  }
120
128
 
129
+ // If encryption is not ready, initialize it now with the authenticated client
130
+ if (!isEncryptionReady()) {
131
+ initializePersistenceEncryption(supabase).catch(err =>
132
+ logger.warn('Failed to initialize encryption with authenticated client', { error: err.message })
133
+ );
134
+ }
135
+
121
136
  // Initialize logger persistence for this request
122
137
  Logger.setPersistence(supabase, user.id);
123
138
 
@@ -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, setEncryptionKey } from '../utils/encryption.js';
10
+ import { initializePersistenceEncryption } from '../services/encryptionInit.js';
11
11
 
12
12
  const router = Router();
13
13
  const logger = createLogger('AuthRoutes');
@@ -24,53 +24,8 @@ router.post('/imap/connect',
24
24
  smtpHost, smtpPort, smtpSecure
25
25
  } = req.body;
26
26
 
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!
60
- .from('user_settings')
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.');
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.');
73
- }
27
+ // Reconcile encryption key with database before saving credentials
28
+ await initializePersistenceEncryption(req.supabase);
74
29
 
75
30
  const imapService = getImapService();
76
31
  const imapConfig = {
@@ -0,0 +1,170 @@
1
+ import crypto from 'crypto';
2
+ import { setEncryptionKey, getEncryptionKeyHex } from '../utils/encryption.js';
3
+ import { getServiceRoleSupabase } from './supabase.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const logger = createLogger('EncryptionInit');
7
+
8
+ let isEncryptionInitialized = false;
9
+
10
+ /**
11
+ * Initialize encryption key from Supabase or generate a new one.
12
+ * In BYOK mode, this might be called multiple times as different Supabase clients become available.
13
+ *
14
+ * @param providedSupabase Optional Supabase client (service role recommended)
15
+ */
16
+ export async function initializePersistenceEncryption(providedSupabase?: any) {
17
+ try {
18
+ const supabase = providedSupabase || getServiceRoleSupabase();
19
+
20
+ if (!supabase) {
21
+ // BYOK mode: Supabase not configured at startup (credentials come via HTTP headers)
22
+ // If we don't have a key yet, generate a temporary one in memory
23
+ if (!getEncryptionKeyHex()) {
24
+ logger.info('Supabase not configured yet (BYOK mode) - using temporary key');
25
+ const newKey = crypto.randomBytes(32).toString('hex');
26
+ setEncryptionKey(newKey);
27
+ }
28
+ return;
29
+ }
30
+
31
+ // Check client type for logging
32
+ const isServiceRole = !!(supabase as any).supabaseServiceRoleKey || !(supabase as any).auth?.session;
33
+ const hasToken = !!(supabase as any).realtime?.accessToken; // Simple check for authenticated client
34
+
35
+ if (!isServiceRole && !hasToken) {
36
+ logger.debug('Skipping encryption reconciliation with unauthenticated anon client');
37
+ return;
38
+ }
39
+
40
+ logger.info(`Reconciling encryption key with database (${isServiceRole ? 'Service Role' : 'Authenticated User'})`);
41
+
42
+ // 1. Check if ANY user has an encryption key stored
43
+ // In sandbox mode, encryption key is always in database
44
+ const { data: users, error } = await supabase
45
+ .from('user_settings')
46
+ .select('user_id, encryption_key')
47
+ .not('encryption_key', 'is', null)
48
+ .limit(1);
49
+
50
+ if (error) {
51
+ logger.warn('Failed to query user_settings for encryption key', { error });
52
+ // If no key in memory, generate one as fallback
53
+ if (!getEncryptionKeyHex()) {
54
+ const newKey = crypto.randomBytes(32).toString('hex');
55
+ setEncryptionKey(newKey);
56
+ logger.info('✓ Generated fallback encryption key due to DB error');
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (users && users.length > 0 && users[0].encryption_key) {
62
+ // Found a persisted key in database
63
+ const dbKey = users[0].encryption_key;
64
+ const currentKey = getEncryptionKeyHex();
65
+
66
+ if (currentKey && currentKey !== dbKey) {
67
+ logger.warn('⚠️ In-memory encryption key differs from database. Overwriting with DB key to ensure consistency across instances.');
68
+ }
69
+
70
+ logger.info('✓ Loaded encryption key from database');
71
+ setEncryptionKey(dbKey);
72
+ isEncryptionInitialized = true;
73
+ return;
74
+ }
75
+
76
+ // 2. No key in database - only generate and persist if we are the "Master" (Service Role)
77
+ // or if we've explicitly decided this is a fresh setup.
78
+ if (isServiceRole) {
79
+ let finalKey = getEncryptionKeyHex();
80
+ if (!finalKey) {
81
+ logger.info('No encryption key found in database or memory, generating new key...');
82
+ finalKey = crypto.randomBytes(32).toString('hex');
83
+ setEncryptionKey(finalKey);
84
+ } else {
85
+ logger.info('Persisting in-memory encryption key to database...');
86
+ }
87
+
88
+ // 3. Persist to all existing users in database
89
+ const { data: allUsers } = await supabase
90
+ .from('user_settings')
91
+ .select('user_id')
92
+ .limit(100);
93
+
94
+ if (allUsers && allUsers.length > 0) {
95
+ logger.info(`Saving encryption key to database for ${allUsers.length} user(s)...`);
96
+
97
+ // Update all users with the new key
98
+ const updates = allUsers.map((user: any) =>
99
+ supabase
100
+ .from('user_settings')
101
+ .update({ encryption_key: finalKey })
102
+ .eq('user_id', user.user_id)
103
+ );
104
+
105
+ await Promise.all(updates);
106
+ logger.info('✓ Encryption key saved to database');
107
+ isEncryptionInitialized = true;
108
+ } else {
109
+ logger.info('No users found yet, encryption key loaded in memory');
110
+ logger.info('Key will be persisted when users are created');
111
+ // We don't set isEncryptionInitialized = true here because we still want
112
+ // to try reconciling once a user is actually created/logged in
113
+ }
114
+ } else {
115
+ logger.warn('No encryption key found in database, but cannot persist one with user-restricted client.');
116
+ }
117
+ } catch (err) {
118
+ logger.error('Error initializing encryption:', err);
119
+ // Always ensure we have a key, even if there was an error
120
+ if (!getEncryptionKeyHex()) {
121
+ logger.warn('Generating emergency fallback encryption key');
122
+ const fallbackKey = crypto.randomBytes(32).toString('hex');
123
+ setEncryptionKey(fallbackKey);
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Periodic sync: Persist in-memory encryption key to users without one
130
+ * This handles the "first user" case and any users created before key was persisted
131
+ */
132
+ export async function syncEncryptionKeyToUsers() {
133
+ try {
134
+ const supabase = getServiceRoleSupabase();
135
+ if (!supabase) return;
136
+
137
+ const currentKey = getEncryptionKeyHex();
138
+ if (!currentKey) return; // No key in memory yet
139
+
140
+ // Find users without encryption key
141
+ const { data: usersWithoutKey, error } = await supabase
142
+ .from('user_settings')
143
+ .select('user_id')
144
+ .is('encryption_key', null)
145
+ .limit(100);
146
+
147
+ if (error || !usersWithoutKey || usersWithoutKey.length === 0) {
148
+ return; // No users to update
149
+ }
150
+
151
+ logger.info(`Found ${usersWithoutKey.length} user(s) without encryption key, persisting...`);
152
+
153
+ // Update all users without a key
154
+ const updates = usersWithoutKey.map((user: any) =>
155
+ supabase
156
+ .from('user_settings')
157
+ .update({ encryption_key: currentKey })
158
+ .eq('user_id', user.user_id)
159
+ );
160
+
161
+ await Promise.all(updates);
162
+ logger.info(`✓ Persisted encryption key to ${usersWithoutKey.length} user(s)`);
163
+ } catch (err) {
164
+ logger.warn('Error syncing encryption key to users:', { error: err });
165
+ }
166
+ }
167
+
168
+ export function isEncryptionReady() {
169
+ return isEncryptionInitialized;
170
+ }