@realtimex/email-automator 2.28.0 → 2.28.1
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 +1 -120
- package/api/src/middleware/auth.ts +15 -0
- package/api/src/routes/auth.ts +3 -48
- package/api/src/services/encryptionInit.ts +156 -0
- package/api/src/services/processor.ts +156 -145
- package/dist/api/server.js +2 -105
- package/dist/api/src/middleware/auth.js +12 -0
- package/dist/api/src/routes/auth.js +3 -45
- package/dist/api/src/services/encryptionInit.js +135 -0
- package/dist/api/src/services/processor.js +150 -139
- package/dist/assets/{index-DpVG-8N2.js → index-Xu8f_mT0.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +1 -1
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 {
|
|
@@ -56,6 +57,20 @@ export async function authMiddleware(
|
|
|
56
57
|
const supabaseUrl = isEnvUrlValid ? envUrl : (headerConfig?.url || '');
|
|
57
58
|
const supabaseAnonKey = isEnvKeyValid ? envKey : (headerConfig?.anonKey || '');
|
|
58
59
|
|
|
60
|
+
// If encryption is not ready, try to initialize it using available Supabase config
|
|
61
|
+
if (!isEncryptionReady() && supabaseUrl && supabaseAnonKey) {
|
|
62
|
+
// Note: Ideally we use service role to read encryption_key from user_settings,
|
|
63
|
+
// but even with anon key it might work if RLS allows or if we just use it
|
|
64
|
+
// to check for existence of keys.
|
|
65
|
+
const initClient = createClient(supabaseUrl, supabaseAnonKey, {
|
|
66
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
67
|
+
});
|
|
68
|
+
// Run in background to not block auth
|
|
69
|
+
initializePersistenceEncryption(initClient).catch(err =>
|
|
70
|
+
logger.warn('Failed to initialize encryption in auth middleware', { error: err.message })
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
// Development bypass: skip auth if DISABLE_AUTH=true in non-production
|
|
60
75
|
if (config.security.disableAuth && !config.isProduction) {
|
|
61
76
|
logger.warn('Auth disabled for development - creating mock user');
|
package/api/src/routes/auth.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
28
|
-
|
|
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,156 @@
|
|
|
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)');
|
|
25
|
+
logger.info('Generating temporary encryption key in memory - will be reconciled when Supabase becomes available');
|
|
26
|
+
const newKey = crypto.randomBytes(32).toString('hex');
|
|
27
|
+
setEncryptionKey(newKey);
|
|
28
|
+
logger.info('✓ Temporary encryption key generated and loaded in memory');
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1. Check if ANY user has an encryption key stored
|
|
34
|
+
// In sandbox mode, encryption key is always in database
|
|
35
|
+
const { data: users, error } = await supabase
|
|
36
|
+
.from('user_settings')
|
|
37
|
+
.select('user_id, encryption_key')
|
|
38
|
+
.not('encryption_key', 'is', null)
|
|
39
|
+
.limit(1);
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
logger.warn('Failed to query user_settings for encryption key', { error });
|
|
43
|
+
// If no key in memory, generate one as fallback
|
|
44
|
+
if (!getEncryptionKeyHex()) {
|
|
45
|
+
const newKey = crypto.randomBytes(32).toString('hex');
|
|
46
|
+
setEncryptionKey(newKey);
|
|
47
|
+
logger.info('✓ Generated fallback encryption key due to DB error');
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (users && users.length > 0 && users[0].encryption_key) {
|
|
53
|
+
// Found a persisted key in database
|
|
54
|
+
const dbKey = users[0].encryption_key;
|
|
55
|
+
const currentKey = getEncryptionKeyHex();
|
|
56
|
+
|
|
57
|
+
if (currentKey && currentKey !== dbKey) {
|
|
58
|
+
logger.warn('⚠️ In-memory encryption key differs from database. Overwriting with DB key to ensure consistency across instances.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info('✓ Loaded encryption key from database');
|
|
62
|
+
setEncryptionKey(dbKey);
|
|
63
|
+
isEncryptionInitialized = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. No key in database - use in-memory key or generate and persist
|
|
68
|
+
// This happens on first run or after fresh database setup
|
|
69
|
+
let finalKey = getEncryptionKeyHex();
|
|
70
|
+
if (!finalKey) {
|
|
71
|
+
logger.info('No encryption key found in database or memory, generating new key...');
|
|
72
|
+
finalKey = crypto.randomBytes(32).toString('hex');
|
|
73
|
+
setEncryptionKey(finalKey);
|
|
74
|
+
} else {
|
|
75
|
+
logger.info('Persisting in-memory encryption key to new Supabase instance...');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Persist to all existing users in database
|
|
79
|
+
const { data: allUsers } = await supabase
|
|
80
|
+
.from('user_settings')
|
|
81
|
+
.select('user_id')
|
|
82
|
+
.limit(100);
|
|
83
|
+
|
|
84
|
+
if (allUsers && allUsers.length > 0) {
|
|
85
|
+
logger.info(`Saving encryption key to database for ${allUsers.length} user(s)...`);
|
|
86
|
+
|
|
87
|
+
// Update all users with the new key
|
|
88
|
+
const updates = allUsers.map((user: any) =>
|
|
89
|
+
supabase
|
|
90
|
+
.from('user_settings')
|
|
91
|
+
.update({ encryption_key: finalKey })
|
|
92
|
+
.eq('user_id', user.user_id)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await Promise.all(updates);
|
|
96
|
+
logger.info('✓ Encryption key saved to database');
|
|
97
|
+
} else {
|
|
98
|
+
logger.info('No users found yet, encryption key loaded in memory');
|
|
99
|
+
logger.info('Key will be persisted when users are created');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isEncryptionInitialized = true;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.error('Error initializing encryption:', err);
|
|
105
|
+
// Always ensure we have a key, even if there was an error
|
|
106
|
+
if (!getEncryptionKeyHex()) {
|
|
107
|
+
logger.warn('Generating emergency fallback encryption key');
|
|
108
|
+
const fallbackKey = crypto.randomBytes(32).toString('hex');
|
|
109
|
+
setEncryptionKey(fallbackKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Periodic sync: Persist in-memory encryption key to users without one
|
|
116
|
+
* This handles the "first user" case and any users created before key was persisted
|
|
117
|
+
*/
|
|
118
|
+
export async function syncEncryptionKeyToUsers() {
|
|
119
|
+
try {
|
|
120
|
+
const supabase = getServiceRoleSupabase();
|
|
121
|
+
if (!supabase) return;
|
|
122
|
+
|
|
123
|
+
const currentKey = getEncryptionKeyHex();
|
|
124
|
+
if (!currentKey) return; // No key in memory yet
|
|
125
|
+
|
|
126
|
+
// Find users without encryption key
|
|
127
|
+
const { data: usersWithoutKey, error } = await supabase
|
|
128
|
+
.from('user_settings')
|
|
129
|
+
.select('user_id')
|
|
130
|
+
.is('encryption_key', null)
|
|
131
|
+
.limit(100);
|
|
132
|
+
|
|
133
|
+
if (error || !usersWithoutKey || usersWithoutKey.length === 0) {
|
|
134
|
+
return; // No users to update
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logger.info(`Found ${usersWithoutKey.length} user(s) without encryption key, persisting...`);
|
|
138
|
+
|
|
139
|
+
// Update all users without a key
|
|
140
|
+
const updates = usersWithoutKey.map((user: any) =>
|
|
141
|
+
supabase
|
|
142
|
+
.from('user_settings')
|
|
143
|
+
.update({ encryption_key: currentKey })
|
|
144
|
+
.eq('user_id', user.user_id)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
await Promise.all(updates);
|
|
148
|
+
logger.info(`✓ Persisted encryption key to ${usersWithoutKey.length} user(s)`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger.warn('Error syncing encryption key to users:', { error: err });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isEncryptionReady() {
|
|
155
|
+
return isEncryptionInitialized;
|
|
156
|
+
}
|