@realtimex/email-automator 2.1.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.
Files changed (139) hide show
  1. package/.env.example +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +247 -0
  4. package/api/server.ts +130 -0
  5. package/api/src/config/index.ts +102 -0
  6. package/api/src/middleware/auth.ts +166 -0
  7. package/api/src/middleware/errorHandler.ts +97 -0
  8. package/api/src/middleware/index.ts +4 -0
  9. package/api/src/middleware/rateLimit.ts +87 -0
  10. package/api/src/middleware/validation.ts +118 -0
  11. package/api/src/routes/actions.ts +214 -0
  12. package/api/src/routes/auth.ts +157 -0
  13. package/api/src/routes/emails.ts +144 -0
  14. package/api/src/routes/health.ts +36 -0
  15. package/api/src/routes/index.ts +22 -0
  16. package/api/src/routes/migrate.ts +76 -0
  17. package/api/src/routes/rules.ts +149 -0
  18. package/api/src/routes/settings.ts +229 -0
  19. package/api/src/routes/sync.ts +152 -0
  20. package/api/src/services/eventLogger.ts +52 -0
  21. package/api/src/services/gmail.ts +456 -0
  22. package/api/src/services/intelligence.ts +288 -0
  23. package/api/src/services/microsoft.ts +368 -0
  24. package/api/src/services/processor.ts +596 -0
  25. package/api/src/services/scheduler.ts +255 -0
  26. package/api/src/services/supabase.ts +144 -0
  27. package/api/src/utils/contentCleaner.ts +114 -0
  28. package/api/src/utils/crypto.ts +80 -0
  29. package/api/src/utils/logger.ts +142 -0
  30. package/bin/email-automator-deploy.js +79 -0
  31. package/bin/email-automator-setup.js +144 -0
  32. package/bin/email-automator.js +61 -0
  33. package/dist/assets/index-BQ1uMdFh.js +97 -0
  34. package/dist/assets/index-Dzi17fx5.css +1 -0
  35. package/dist/email-automator-logo.svg +51 -0
  36. package/dist/favicon.svg +45 -0
  37. package/dist/index.html +14 -0
  38. package/index.html +13 -0
  39. package/package.json +112 -0
  40. package/public/email-automator-logo.svg +51 -0
  41. package/public/favicon.svg +45 -0
  42. package/scripts/deploy-functions.sh +55 -0
  43. package/scripts/migrate.sh +177 -0
  44. package/src/App.tsx +622 -0
  45. package/src/components/AccountSettings.tsx +310 -0
  46. package/src/components/AccountSettingsPage.tsx +390 -0
  47. package/src/components/Configuration.tsx +1345 -0
  48. package/src/components/Dashboard.tsx +940 -0
  49. package/src/components/ErrorBoundary.tsx +71 -0
  50. package/src/components/LiveTerminal.tsx +308 -0
  51. package/src/components/LoadingSpinner.tsx +39 -0
  52. package/src/components/Login.tsx +371 -0
  53. package/src/components/Logo.tsx +57 -0
  54. package/src/components/SetupWizard.tsx +388 -0
  55. package/src/components/Toast.tsx +109 -0
  56. package/src/components/migration/MigrationBanner.tsx +97 -0
  57. package/src/components/migration/MigrationModal.tsx +458 -0
  58. package/src/components/migration/MigrationPulseIndicator.tsx +38 -0
  59. package/src/components/mode-toggle.tsx +24 -0
  60. package/src/components/theme-provider.tsx +72 -0
  61. package/src/components/ui/alert.tsx +66 -0
  62. package/src/components/ui/button.tsx +57 -0
  63. package/src/components/ui/card.tsx +75 -0
  64. package/src/components/ui/dialog.tsx +133 -0
  65. package/src/components/ui/input.tsx +22 -0
  66. package/src/components/ui/label.tsx +24 -0
  67. package/src/components/ui/otp-input.tsx +184 -0
  68. package/src/context/AppContext.tsx +422 -0
  69. package/src/context/MigrationContext.tsx +53 -0
  70. package/src/context/TerminalContext.tsx +31 -0
  71. package/src/core/actions.ts +76 -0
  72. package/src/core/auth.ts +108 -0
  73. package/src/core/intelligence.ts +76 -0
  74. package/src/core/processor.ts +112 -0
  75. package/src/hooks/useRealtimeEmails.ts +111 -0
  76. package/src/index.css +140 -0
  77. package/src/lib/api-config.ts +42 -0
  78. package/src/lib/api-old.ts +228 -0
  79. package/src/lib/api.ts +421 -0
  80. package/src/lib/migration-check.ts +264 -0
  81. package/src/lib/sounds.ts +120 -0
  82. package/src/lib/supabase-config.ts +117 -0
  83. package/src/lib/supabase.ts +28 -0
  84. package/src/lib/types.ts +166 -0
  85. package/src/lib/utils.ts +6 -0
  86. package/src/main.tsx +10 -0
  87. package/supabase/.env.example +15 -0
  88. package/supabase/.temp/cli-latest +1 -0
  89. package/supabase/.temp/gotrue-version +1 -0
  90. package/supabase/.temp/pooler-url +1 -0
  91. package/supabase/.temp/postgres-version +1 -0
  92. package/supabase/.temp/project-ref +1 -0
  93. package/supabase/.temp/rest-version +1 -0
  94. package/supabase/.temp/storage-migration +1 -0
  95. package/supabase/.temp/storage-version +1 -0
  96. package/supabase/config.toml +95 -0
  97. package/supabase/functions/_shared/auth-helper.ts +76 -0
  98. package/supabase/functions/_shared/auth.ts +33 -0
  99. package/supabase/functions/_shared/cors.ts +45 -0
  100. package/supabase/functions/_shared/encryption.ts +70 -0
  101. package/supabase/functions/_shared/supabaseAdmin.ts +14 -0
  102. package/supabase/functions/api-v1-accounts/index.ts +133 -0
  103. package/supabase/functions/api-v1-emails/index.ts +177 -0
  104. package/supabase/functions/api-v1-rules/index.ts +177 -0
  105. package/supabase/functions/api-v1-settings/index.ts +247 -0
  106. package/supabase/functions/auth-gmail/index.ts +197 -0
  107. package/supabase/functions/auth-microsoft/index.ts +215 -0
  108. package/supabase/functions/setup/index.ts +92 -0
  109. package/supabase/migrations/20260114000000_initial_schema.sql +81 -0
  110. package/supabase/migrations/20260115000000_add_user_settings.sql +49 -0
  111. package/supabase/migrations/20260115000001_add_auth_flow.sql +80 -0
  112. package/supabase/migrations/20260115000002_fix_permissions.sql +5 -0
  113. package/supabase/migrations/20260115000003_fix_init_state_permissions.sql +9 -0
  114. package/supabase/migrations/20260115000004_add_migration_rpc.sql +13 -0
  115. package/supabase/migrations/20260115000005_add_provider_creds.sql +7 -0
  116. package/supabase/migrations/20260115000006_backfill_profiles.sql +22 -0
  117. package/supabase/migrations/20260116000000_add_sync_scope.sql +15 -0
  118. package/supabase/migrations/20260116000001_per_account_sync_scope.sql +19 -0
  119. package/supabase/migrations/20260116000002_add_llm_api_key.sql +5 -0
  120. package/supabase/migrations/20260117000000_refactor_integrations.sql +36 -0
  121. package/supabase/migrations/20260117000001_add_processing_events.sql +30 -0
  122. package/supabase/migrations/20260117000002_multi_actions.sql +15 -0
  123. package/supabase/migrations/20260117000003_seed_default_rules.sql +77 -0
  124. package/supabase/migrations/20260117000004_rule_instructions.sql +5 -0
  125. package/supabase/migrations/20260117000005_rule_attachments.sql +7 -0
  126. package/supabase/migrations/20260117000006_setup_storage.sql +32 -0
  127. package/supabase/migrations/20260117000007_add_system_logs.sql +26 -0
  128. package/supabase/migrations/20260117000008_link_logs_to_accounts.sql +8 -0
  129. package/supabase/migrations/20260117000009_convert_toggles_to_rules.sql +28 -0
  130. package/supabase/migrations/20260117000010_add_atomic_action_append.sql +13 -0
  131. package/supabase/migrations/20260117000011_add_profile_avatar.sql +4 -0
  132. package/supabase/migrations/20260117000012_setup_avatars_storage.sql +26 -0
  133. package/supabase/templates/confirmation.html +76 -0
  134. package/supabase/templates/email-change.html +76 -0
  135. package/supabase/templates/invite.html +72 -0
  136. package/supabase/templates/magic-link.html +68 -0
  137. package/supabase/templates/recovery.html +82 -0
  138. package/tsconfig.json +36 -0
  139. package/vite.config.ts +162 -0
package/.env.example ADDED
@@ -0,0 +1,35 @@
1
+ # Supabase Configuration
2
+ VITE_SUPABASE_URL=your_supabase_url
3
+ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
4
+
5
+ # API Configuration
6
+ # Express API URL (default: http://localhost:3004)
7
+ # Note: RealTimeX Desktop uses ports 3001/3002
8
+ VITE_API_URL=http://localhost:3004
9
+ PORT=3004
10
+
11
+ # OpenAI / LLM Configuration
12
+ LLM_API_KEY=your_llm_api_key
13
+ LLM_BASE_URL=https://api.openai.com/v1
14
+ LLM_MODEL=gpt-4o-mini
15
+
16
+ # Security (required in production)
17
+ JWT_SECRET="your-secure-jwt-secret-min-32-chars"
18
+ TOKEN_ENCRYPTION_KEY="your-32-character-encryption-key"
19
+ CORS_ORIGINS="https://yourdomain.com"
20
+
21
+ # Development Only - Bypass authentication (DO NOT use in production)
22
+ DISABLE_AUTH=true
23
+
24
+ # Gmail OAuth
25
+ GMAIL_CLIENT_ID=your_gmail_client_id
26
+ GMAIL_CLIENT_SECRET=your_gmail_client_secret
27
+
28
+ # Microsoft Graph (Outlook)
29
+ MS_GRAPH_CLIENT_ID=your_ms_graph_client_id
30
+ MS_GRAPH_TENANT_ID=common
31
+ MS_GRAPH_CLIENT_SECRET=your_ms_graph_client_secret
32
+
33
+ # Processing
34
+ EMAIL_BATCH_SIZE=20
35
+ SYNC_INTERVAL_MS=300000
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RealTimeX Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # AI Email Automator v2.0
2
+
3
+ An agentic, AI-powered email management platform that learns to handle your inbox. Categorize, archive, delete, and draft responses automatically using LLMs and Supabase.
4
+
5
+ ## 🚀 Vision: "Own Your Inbox"
6
+
7
+ The AI Email Automator is designed as a standalone "Agent" for the RealTimeX ecosystem. It follows the **Own Your Data** philosophy:
8
+
9
+ - **Zero Cloud Costs**: Runs on your infrastructure using Supabase
10
+ - **Privacy First**: Your emails are processed on your private infrastructure
11
+ - **Agentic Intelligence**: An assistant that suggests "Winning Responses"
12
+ - **Production Ready**: Full security, testing, and DevOps infrastructure
13
+
14
+ ## ✨ Features
15
+
16
+ ### Core Capabilities
17
+ - **Smart Categorization**: AI classifies emails (Spam, Newsletter, Support, Client, etc.)
18
+ - **Inbox Zero Engine**: Auto-trash spam and archive newsletters
19
+ - **Winning Responses**: AI-generated draft replies for important emails
20
+ - **Multi-Provider**: Gmail (OAuth2) and Microsoft 365 (Device Flow)
21
+ - **Automation Rules**: Custom rules for auto-pilot email handling
22
+ - **Real-time Sync**: Live updates via Supabase subscriptions
23
+ - **Background Scheduler**: Automatic periodic email sync
24
+
25
+ ### Production Features
26
+ - **Security**: JWT auth, token encryption, rate limiting, input validation
27
+ - **Hybrid Architecture**: Edge Functions (serverless) + Local App (privacy)
28
+ - **State Management**: React Context with centralized app state
29
+ - **Error Handling**: Error boundaries, toast notifications, logging
30
+ - **Analytics Dashboard**: Email stats, category breakdown, sync history
31
+ - **RealTimeX Integration**: Works with RealTimeX Desktop as Local App
32
+
33
+ ## 🛠 Tech Stack
34
+
35
+ | Layer | Technologies |
36
+ |-------|-------------|
37
+ | **Frontend** | React 19, Vite 7, TailwindCSS 4, Lucide Icons |
38
+ | **Edge Functions** | Deno, Supabase Functions (OAuth, DB proxy) |
39
+ | **Local API** | Node.js, Express 5, TypeScript (Sync, AI) |
40
+ | **AI** | OpenAI / Instructor-JS (supports Ollama, LM Studio) |
41
+ | **Database** | Supabase (PostgreSQL) with RLS |
42
+ | **Testing** | Vitest, Testing Library |
43
+
44
+ ## 🏁 Quick Start
45
+
46
+ For detailed instructions on installation, configuration, and usage, please see the [**Email Automator Documentation Hub**](docs/README.md).
47
+
48
+ ### Prerequisites
49
+ - Node.js v20+
50
+ - Supabase project with CLI access
51
+ - LLM API key (OpenAI, Anthropic, or local)
52
+
53
+ ### Option 1: Using npx (Recommended)
54
+
55
+ ```bash
56
+ # Interactive setup
57
+ npx @realtimex/email-automator-setup
58
+
59
+ # Deploy Edge Functions
60
+ npx @realtimex/email-automator-deploy
61
+
62
+ # Start Email Automator
63
+ npx @realtimex/email-automator
64
+ ```
65
+
66
+ ### Option 2: Clone and Install
67
+
68
+ ```bash
69
+ git clone https://github.com/therealtimex/email-automator.git
70
+ cd email-automator
71
+ npm install
72
+ ```
73
+
74
+ ### Setup
75
+
76
+ 1. **Deploy Edge Functions to Supabase:**
77
+ ```bash
78
+ supabase login
79
+ ./scripts/deploy-functions.sh
80
+ ```
81
+
82
+ 2. **Configure Edge Function Secrets in Supabase Dashboard:**
83
+ - Settings → Edge Functions → Add secrets
84
+ - Required: `TOKEN_ENCRYPTION_KEY`, `GMAIL_CLIENT_ID`, `GMAIL_CLIENT_SECRET`, etc.
85
+
86
+ 3. **Configure Local Environment:**
87
+ ```bash
88
+ cp .env.example .env
89
+ # Edit .env with your credentials
90
+ ```
91
+
92
+ ### Development
93
+
94
+ ```bash
95
+ # Terminal 1: Local API (Email Sync & AI Processing)
96
+ # Default port: 3004 (RealTimeX Desktop uses 3001/3002)
97
+ npm run dev:api
98
+
99
+ # Terminal 2: Frontend
100
+ npm run dev
101
+
102
+ # Optional: Specify custom ports
103
+ npm run dev:api -- --port 3005
104
+ npm run dev -- --port 5174
105
+ ```
106
+
107
+ **Note:** Email Automator uses port **3004** by default to avoid conflicts with RealTimeX Desktop (ports 3001/3002). You can change ports via command line arguments or environment variables.
108
+
109
+ ### Using npx
110
+
111
+ ```bash
112
+ # Start with default port (3004)
113
+ npx @realtimex/email-automator
114
+
115
+ # Start with custom port
116
+ npx @realtimex/email-automator --port 3005
117
+ ```
118
+
119
+ See [NPX Usage Guide](docs-dev/NPX-USAGE.md) for complete documentation.
120
+
121
+ ## 📂 Project Structure
122
+
123
+ ```
124
+ ├── api/ # Local App (Express)
125
+ │ ├── server.ts # Express entry point
126
+ │ └── src/
127
+ │ ├── routes/ # Sync & Actions endpoints
128
+ │ ├── services/ # Email sync, AI processing
129
+ │ └── utils/ # Logger, crypto, helpers
130
+ ├── src/ # Frontend (React)
131
+ │ ├── components/ # React components
132
+ │ ├── context/ # App state management
133
+ │ ├── hooks/ # Custom hooks (realtime)
134
+ │ └── lib/ # Hybrid API client, types
135
+ ├── supabase/ # Supabase Configuration
136
+ │ ├── functions/ # Edge Functions (OAuth, DB)
137
+ │ │ ├── _shared/ # Shared utilities
138
+ │ │ ├── auth-gmail/ # Gmail OAuth
139
+ │ │ ├── auth-microsoft/ # Microsoft OAuth
140
+ │ │ ├── api-v1-accounts/ # Account management
141
+ │ │ ├── api-v1-emails/ # Email operations
142
+ │ │ ├── api-v1-rules/ # Rules CRUD
143
+ │ │ └── api-v1-settings/ # Settings & stats
144
+ │ └── migrations/ # Database schema
145
+ ├── scripts/
146
+ │ └── deploy-functions.sh # Deploy Edge Functions
147
+ └── tests/ # Unit & integration tests
148
+ ```
149
+
150
+ ## 🔐 Environment Variables
151
+
152
+ ### Edge Functions (Supabase Dashboard)
153
+ ```bash
154
+ TOKEN_ENCRYPTION_KEY=32-char-key
155
+ GMAIL_CLIENT_ID=xxx
156
+ GMAIL_CLIENT_SECRET=xxx
157
+ MS_GRAPH_CLIENT_ID=xxx
158
+ MS_GRAPH_CLIENT_SECRET=xxx
159
+ ```
160
+
161
+ ### Local App (.env file)
162
+ ```bash
163
+ # Supabase
164
+ VITE_SUPABASE_URL=https://xxx.supabase.co
165
+ VITE_SUPABASE_ANON_KEY=your-anon-key
166
+
167
+ # API Configuration (default port: 3004)
168
+ VITE_API_URL=http://localhost:3004
169
+ PORT=3004
170
+
171
+ # LLM
172
+ LLM_API_KEY=your-llm-key
173
+ LLM_BASE_URL=https://api.openai.com/v1
174
+ LLM_MODEL=gpt-4o-mini
175
+
176
+ # Development
177
+ DISABLE_AUTH=true
178
+ ```
179
+
180
+ ## 🧪 Testing
181
+
182
+ ```bash
183
+ npm run test # Watch mode
184
+ npm run test:run # Single run
185
+ npm run test:coverage # With coverage
186
+ ```
187
+
188
+ ## 📡 API Architecture
189
+
190
+ ### Edge Functions (Supabase)
191
+ | Method | Endpoint | Description |
192
+ |--------|----------|-------------|
193
+ | GET | `/auth-gmail?action=url` | Get Gmail OAuth URL |
194
+ | POST | `/auth-gmail` | Complete Gmail OAuth |
195
+ | POST | `/auth-microsoft?action=device-flow` | Start Microsoft device flow |
196
+ | GET | `/api-v1-accounts` | List connected accounts |
197
+ | GET | `/api-v1-emails` | List processed emails |
198
+ | GET | `/api-v1-rules` | List automation rules |
199
+ | GET | `/api-v1-settings` | Get user settings |
200
+
201
+ ### Local App (Express)
202
+ | Method | Endpoint | Description |
203
+ |--------|----------|-------------|
204
+ | POST | `/api/sync` | Trigger email sync |
205
+ | POST | `/api/actions/execute` | Execute email action |
206
+ | POST | `/api/actions/draft/:id` | Generate draft reply |
207
+ | GET | `/api/health` | Health check |
208
+
209
+ ## 🤝 Contributing
210
+
211
+ 1. Fork the repository
212
+ 2. Create a feature branch
213
+ 3. Make your changes
214
+ 4. Run tests: `npm run test:run`
215
+ 5. Submit a Pull Request
216
+
217
+ ## 📄 License
218
+
219
+ MIT License - Copyright (c) 2026 RealTimeX Team
220
+
221
+ ## 📦 NPX Commands
222
+
223
+ Email Automator is fully compatible with npx for easy installation and execution:
224
+
225
+ | Command | Description |
226
+ |---------|-------------|
227
+ | `npx @realtimex/email-automator` | Start the Email Automator API server |
228
+ | `npx @realtimex/email-automator-setup` | Interactive setup wizard |
229
+ | `npx @realtimex/email-automator-deploy` | Deploy Edge Functions to Supabase |
230
+
231
+ ### Examples
232
+
233
+ ```bash
234
+ # First time setup
235
+ npx @realtimex/email-automator-setup
236
+ npx @realtimex/email-automator-deploy
237
+ npx @realtimex/email-automator
238
+
239
+ # Daily usage
240
+ npx @realtimex/email-automator
241
+
242
+ # Custom port
243
+ npx @realtimex/email-automator --port 3005
244
+ ```
245
+
246
+ See [NPX Usage Guide](docs-dev/NPX-USAGE.md) for complete documentation.
247
+
package/api/server.ts ADDED
@@ -0,0 +1,130 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { spawn } from 'child_process';
6
+ import { config, validateConfig } from './src/config/index.js';
7
+ import { errorHandler } from './src/middleware/errorHandler.js';
8
+ import { apiRateLimit } from './src/middleware/rateLimit.js';
9
+ import routes from './src/routes/index.js';
10
+ import { logger } from './src/utils/logger.js';
11
+ import { getServerSupabase } from './src/services/supabase.js';
12
+ import { startScheduler, stopScheduler } from './src/services/scheduler.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ // Validate configuration
18
+ const configValidation = validateConfig();
19
+ if (!configValidation.valid) {
20
+ logger.warn('Configuration warnings', { errors: configValidation.errors });
21
+ }
22
+
23
+ const app = express();
24
+
25
+ // Security headers
26
+ app.use((req, res, next) => {
27
+ res.setHeader('X-Content-Type-Options', 'nosniff');
28
+ res.setHeader('X-Frame-Options', 'DENY');
29
+ res.setHeader('X-XSS-Protection', '1; mode=block');
30
+ if (config.isProduction) {
31
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
32
+ }
33
+ next();
34
+ });
35
+
36
+ // CORS configuration
37
+ app.use(cors({
38
+ origin: config.isProduction
39
+ ? config.security.corsOrigins
40
+ : true, // Allow all in development
41
+ credentials: true,
42
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
43
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Supabase-Url', 'X-Supabase-Anon-Key'],
44
+ }));
45
+
46
+ // Body parsing
47
+ app.use(express.json({ limit: '10mb' }));
48
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
49
+
50
+ // Request logging
51
+ app.use((req, res, next) => {
52
+ const start = Date.now();
53
+ res.on('finish', () => {
54
+ const duration = Date.now() - start;
55
+ logger.debug(`${req.method} ${req.path}`, {
56
+ status: res.statusCode,
57
+ duration: `${duration}ms`,
58
+ });
59
+ });
60
+ next();
61
+ });
62
+
63
+ // Rate limiting (global)
64
+ app.use('/api', apiRateLimit);
65
+
66
+ // API routes
67
+ app.use('/api', routes);
68
+
69
+ // Serve static files in production or if dist exists
70
+ const distPath = path.join(__dirname, '..', 'dist');
71
+ app.use(express.static(distPath));
72
+
73
+ // Handle client-side routing
74
+ app.get(/.*/, (req, res, next) => {
75
+ if (req.path.startsWith('/api')) return next();
76
+ res.sendFile(path.join(distPath, 'index.html'), (err) => {
77
+ if (err) {
78
+ // If dist doesn't exist, return 404 for non-API routes
79
+ res.status(404).json({
80
+ success: false,
81
+ error: { code: 'NOT_FOUND', message: 'Frontend not built or endpoint not found' }
82
+ });
83
+ }
84
+ });
85
+ });
86
+
87
+ // Error handler (must be last)
88
+ app.use(errorHandler);
89
+
90
+ // Graceful shutdown
91
+ const shutdown = () => {
92
+ logger.info('Shutting down gracefully...');
93
+ stopScheduler();
94
+ process.exit(0);
95
+ };
96
+
97
+ process.on('SIGTERM', shutdown);
98
+ process.on('SIGINT', shutdown);
99
+
100
+ // Start server
101
+ const server = app.listen(config.port, () => {
102
+ const url = `http://localhost:${config.port}`;
103
+ logger.info(`Server running at ${url}`, {
104
+ environment: config.nodeEnv,
105
+ supabase: getServerSupabase() ? 'connected' : 'not configured',
106
+ });
107
+
108
+ // Start background scheduler
109
+ if (getServerSupabase()) {
110
+ startScheduler();
111
+ }
112
+
113
+ // Automatically open browser unless -n flag is provided
114
+ if (!config.noUi) {
115
+ const startCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
116
+ spawn(startCommand, [url], { detached: true }).unref();
117
+ }
118
+ });
119
+
120
+ // Handle server errors
121
+ server.on('error', (error: NodeJS.ErrnoException) => {
122
+ if (error.code === 'EADDRINUSE') {
123
+ logger.error(`Port ${config.port} is already in use`);
124
+ } else {
125
+ logger.error('Server error', error);
126
+ }
127
+ process.exit(1);
128
+ });
129
+
130
+ export default app;
@@ -0,0 +1,102 @@
1
+ import dotenv from 'dotenv';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ dotenv.config();
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ function parseArgs(args: string[]): { port: number | null, noUi: boolean } {
11
+ const portIndex = args.indexOf('--port');
12
+ let port = null;
13
+ if (portIndex !== -1 && args[portIndex + 1]) {
14
+ const p = parseInt(args[portIndex + 1], 10);
15
+ if (!isNaN(p) && p > 0 && p < 65536) {
16
+ port = p;
17
+ }
18
+ }
19
+
20
+ const noUi = args.includes('--no-ui');
21
+
22
+ return { port, noUi };
23
+ }
24
+
25
+ const cliArgs = parseArgs(process.argv.slice(2));
26
+
27
+ export const config = {
28
+ // Server
29
+ // Default port 3004 (RealTimeX Desktop uses 3001/3002)
30
+ port: cliArgs.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3004),
31
+ noUi: cliArgs.noUi,
32
+ nodeEnv: process.env.NODE_ENV || 'development',
33
+ isProduction: process.env.NODE_ENV === 'production',
34
+
35
+ // Paths
36
+ rootDir: join(__dirname, '..', '..', '..'),
37
+ scriptsDir: join(__dirname, '..', '..', '..', 'scripts'),
38
+
39
+ // Supabase
40
+ supabase: {
41
+ url: process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || '',
42
+ anonKey: process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || '',
43
+ serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '',
44
+ },
45
+
46
+ // LLM
47
+ llm: {
48
+ apiKey: process.env.LLM_API_KEY || '',
49
+ baseUrl: process.env.LLM_BASE_URL,
50
+ model: process.env.LLM_MODEL || 'gpt-4o-mini',
51
+ },
52
+
53
+ // OAuth - Gmail
54
+ gmail: {
55
+ clientId: process.env.GMAIL_CLIENT_ID || '',
56
+ clientSecret: process.env.GMAIL_CLIENT_SECRET || '',
57
+ redirectUri: process.env.GMAIL_REDIRECT_URI || 'urn:ietf:wg:oauth:2.0:oob',
58
+ },
59
+
60
+ // OAuth - Microsoft
61
+ microsoft: {
62
+ clientId: process.env.MS_GRAPH_CLIENT_ID || '',
63
+ tenantId: process.env.MS_GRAPH_TENANT_ID || 'common',
64
+ clientSecret: process.env.MS_GRAPH_CLIENT_SECRET || '',
65
+ },
66
+
67
+ // Security
68
+ security: {
69
+ encryptionKey: process.env.TOKEN_ENCRYPTION_KEY || '',
70
+ jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
71
+ corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3003', 'http://localhost:5173'],
72
+ rateLimitWindowMs: 15 * 60 * 1000, // 15 minutes
73
+ rateLimitMax: 100,
74
+ disableAuth: process.env.DISABLE_AUTH === 'true',
75
+ },
76
+
77
+ // Processing
78
+ processing: {
79
+ batchSize: parseInt(process.env.EMAIL_BATCH_SIZE || '20', 10),
80
+ syncIntervalMs: parseInt(process.env.SYNC_INTERVAL_MS || '60000', 10), // 1 minute
81
+ maxRetries: 3,
82
+ },
83
+ };
84
+
85
+ export function validateConfig(): { valid: boolean; errors: string[] } {
86
+ const errors: string[] = [];
87
+
88
+ if (!config.supabase.url) {
89
+ errors.push('SUPABASE_URL is required');
90
+ }
91
+ if (!config.supabase.anonKey) {
92
+ errors.push('SUPABASE_ANON_KEY is required');
93
+ }
94
+ if (config.isProduction && config.security.jwtSecret === 'dev-secret-change-in-production') {
95
+ errors.push('JWT_SECRET must be set in production');
96
+ }
97
+ if (config.isProduction && !config.security.encryptionKey) {
98
+ errors.push('TOKEN_ENCRYPTION_KEY must be set in production');
99
+ }
100
+
101
+ return { valid: errors.length === 0, errors };
102
+ }
@@ -0,0 +1,166 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
3
+ import { config } from '../config/index.js';
4
+ import { AuthenticationError, AuthorizationError } from './errorHandler.js';
5
+ import { createLogger, Logger } from '../utils/logger.js';
6
+
7
+ const logger = createLogger('AuthMiddleware');
8
+
9
+ import { getServerSupabase, isValidUrl } from '../services/supabase.js';
10
+
11
+ // Extend Express Request to include user
12
+ declare global {
13
+ namespace Express {
14
+ interface Request {
15
+ user?: User;
16
+ supabase?: SupabaseClient;
17
+ }
18
+ }
19
+ }
20
+
21
+ // Check if anon key looks valid (JWT or publishable key format)
22
+ function isValidAnonKey(key: string): boolean {
23
+ if (!key) return false;
24
+ // JWT anon keys start with eyJ, publishable keys start with sb_publishable_
25
+ return key.startsWith('eyJ') || key.startsWith('sb_publishable_');
26
+ }
27
+
28
+ // Helper to get Supabase config from request headers (frontend passes these)
29
+ function getSupabaseConfigFromRequest(req: Request): { url: string; anonKey: string } | null {
30
+ const url = req.headers['x-supabase-url'] as string;
31
+ const anonKey = req.headers['x-supabase-anon-key'] as string;
32
+
33
+ if (url && anonKey && isValidUrl(url) && isValidAnonKey(anonKey)) {
34
+ return { url, anonKey };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export async function authMiddleware(
40
+ req: Request,
41
+ _res: Response,
42
+ next: NextFunction
43
+ ): Promise<void> {
44
+ try {
45
+ // Get Supabase config: prefer env vars, fallback to request headers
46
+ const headerConfig = getSupabaseConfigFromRequest(req);
47
+
48
+ const envUrl = config.supabase.url;
49
+ const envKey = config.supabase.anonKey;
50
+
51
+ // Basic validation: URL must start with http(s)
52
+ // This prevents using placeholders like "CHANGE_ME" or empty strings
53
+ const isEnvUrlValid = envUrl && (envUrl.startsWith('http://') || envUrl.startsWith('https://'));
54
+ const isEnvKeyValid = !!envKey && envKey.length > 0;
55
+
56
+ const supabaseUrl = isEnvUrlValid ? envUrl : (headerConfig?.url || '');
57
+ const supabaseAnonKey = isEnvKeyValid ? envKey : (headerConfig?.anonKey || '');
58
+
59
+ // Development bypass: skip auth if DISABLE_AUTH=true in non-production
60
+ if (config.security.disableAuth && !config.isProduction) {
61
+ logger.warn('Auth disabled for development - creating mock user');
62
+
63
+ // Create a mock user for development
64
+ req.user = {
65
+ id: '00000000-0000-0000-0000-000000000000',
66
+ email: 'dev@local.test',
67
+ user_metadata: {},
68
+ app_metadata: {},
69
+ aud: 'authenticated',
70
+ created_at: new Date().toISOString(),
71
+ } as User;
72
+
73
+ // Use the shared Supabase client, or create one from request headers
74
+ let supabase = getServerSupabase();
75
+ if (!supabase && supabaseUrl && supabaseAnonKey) {
76
+ supabase = createClient(supabaseUrl, supabaseAnonKey, {
77
+ auth: { autoRefreshToken: false, persistSession: false },
78
+ });
79
+ }
80
+
81
+ if (supabase) {
82
+ req.supabase = supabase;
83
+ // Initialize logger persistence for mock user
84
+ Logger.setPersistence(supabase, req.user.id);
85
+ } else {
86
+ throw new AuthenticationError('Supabase not configured. Please set up Supabase in the app or provide SUPABASE_URL/ANON_KEY in .env');
87
+ }
88
+
89
+ return next();
90
+ }
91
+
92
+ const authHeader = req.headers.authorization;
93
+
94
+ if (!authHeader?.startsWith('Bearer ')) {
95
+ throw new AuthenticationError('Missing or invalid authorization header');
96
+ }
97
+
98
+ const token = authHeader.substring(7);
99
+
100
+ if (!supabaseUrl || !supabaseAnonKey) {
101
+ throw new AuthenticationError('Supabase not configured. Please set up Supabase in the app or provide SUPABASE_URL/ANON_KEY in .env');
102
+ }
103
+
104
+ // Create a Supabase client with the user's token
105
+ const supabase = createClient(supabaseUrl, supabaseAnonKey, {
106
+ global: {
107
+ headers: {
108
+ Authorization: `Bearer ${token}`,
109
+ },
110
+ },
111
+ });
112
+
113
+ // Verify the token by getting the user
114
+ const { data: { user }, error } = await supabase.auth.getUser(token);
115
+
116
+ if (error || !user) {
117
+ logger.debug('Auth failed', { error: error?.message });
118
+ throw new AuthenticationError('Invalid or expired token');
119
+ }
120
+
121
+ // Initialize logger persistence for this request
122
+ Logger.setPersistence(supabase, user.id);
123
+
124
+ // Attach user and supabase client to request
125
+ req.user = user;
126
+ req.supabase = supabase;
127
+
128
+ next();
129
+ } catch (error) {
130
+ logger.error('Auth middleware error', error);
131
+ next(error);
132
+ }
133
+ }
134
+
135
+ export function optionalAuth(
136
+ req: Request,
137
+ _res: Response,
138
+ next: NextFunction
139
+ ): void {
140
+ const authHeader = req.headers.authorization;
141
+
142
+ if (!authHeader?.startsWith('Bearer ')) {
143
+ // No auth provided, continue without user
144
+ return next();
145
+ }
146
+
147
+ // If auth is provided, validate it
148
+ authMiddleware(req, _res, next);
149
+ }
150
+
151
+ export function requireRole(roles: string[]) {
152
+ return async (req: Request, _res: Response, next: NextFunction) => {
153
+ if (!req.user) {
154
+ return next(new AuthenticationError());
155
+ }
156
+
157
+ // Check user metadata for role (customize based on your auth setup)
158
+ const userRole = req.user.user_metadata?.role || 'user';
159
+
160
+ if (!roles.includes(userRole)) {
161
+ return next(new AuthorizationError(`Requires one of: ${roles.join(', ')}`));
162
+ }
163
+
164
+ next();
165
+ };
166
+ }