@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 +1 -120
- package/api/src/middleware/auth.ts +15 -0
- package/api/src/routes/auth.ts +3 -48
- package/api/src/services/encryptionInit.ts +170 -0
- package/api/src/services/processor.ts +156 -145
- package/dist/api/server.js +2 -105
- package/dist/api/src/middleware/auth.js +9 -0
- package/dist/api/src/routes/auth.js +3 -45
- package/dist/api/src/services/encryptionInit.js +148 -0
- package/dist/api/src/services/processor.js +150 -139
- package/dist/assets/{index-DpVG-8N2.js → index-CRx9ZPz1.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 {
|
|
@@ -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
|
|
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,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
|
+
}
|