avocavo 1.0.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.
@@ -0,0 +1,718 @@
1
+ const { createClient } = require('@supabase/supabase-js');
2
+ const axios = require('axios');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const open = require('open');
6
+ const Conf = require('conf');
7
+ const http = require('http');
8
+ const url = require('url');
9
+ const readline = require('readline');
10
+
11
+ // Try to load keytar, fall back gracefully if unavailable
12
+ let keytar;
13
+ let keytarAvailable = false;
14
+ try {
15
+ keytar = require('keytar');
16
+ keytarAvailable = true;
17
+ } catch (error) {
18
+ console.warn(chalk.yellow('āš ļø Secure storage unavailable, using config file storage'));
19
+ keytarAvailable = false;
20
+ }
21
+
22
+ class SupabaseAuthManager {
23
+ constructor(baseUrl = 'https://app.avocavo.app') {
24
+ this.baseUrl = baseUrl.replace(/\/$/, '');
25
+ this.serviceName = 'avocavo-nutrition';
26
+ this.keytarAvailable = keytarAvailable;
27
+
28
+ // Keep config for non-sensitive metadata
29
+ this.config = new Conf({
30
+ projectName: 'avocavo-nutrition',
31
+ configName: 'auth'
32
+ });
33
+
34
+ // Initialize Supabase client - get config from backend
35
+ this.supabaseConfig = null;
36
+ this.supabase = null;
37
+ }
38
+
39
+ async initializeSupabase() {
40
+ if (this.supabase) return true;
41
+
42
+ try {
43
+ const spinner = ora('Getting Supabase configuration...').start();
44
+ const response = await axios.get(`${this.baseUrl}/api/auth/supabase-config`, { timeout: 10000 });
45
+
46
+ if (!response.data.success) {
47
+ spinner.fail('Failed to get Supabase configuration');
48
+ console.error(chalk.red(`āŒ ${response.data.error}`));
49
+ return false;
50
+ }
51
+
52
+ this.supabaseConfig = response.data.config;
53
+ this.supabase = createClient(this.supabaseConfig.url, this.supabaseConfig.anon_key);
54
+
55
+ spinner.succeed('Supabase configuration loaded');
56
+ return true;
57
+ } catch (error) {
58
+ console.error(chalk.red(`āŒ Failed to initialize Supabase: ${error.message}`));
59
+ return false;
60
+ }
61
+ }
62
+
63
+ async login(provider = 'google') {
64
+ console.log(chalk.cyan(`šŸ” Starting ${provider} OAuth login with Supabase...`));
65
+
66
+ // Initialize Supabase client first
67
+ const initialized = await this.initializeSupabase();
68
+ if (!initialized) {
69
+ return false;
70
+ }
71
+
72
+ try {
73
+ // Create a temporary local server to handle the OAuth callback
74
+ const server = await this.createCallbackServer();
75
+ const callbackUrl = `http://localhost:${server.port}/callback`;
76
+
77
+ console.log(chalk.cyan('🌐 Opening browser for authentication...'));
78
+
79
+ // Start OAuth flow with Supabase
80
+ // console.log(chalk.blue(`šŸ” OAuth callback URL: ${callbackUrl}`)); // Debug only
81
+ const { data, error } = await this.supabase.auth.signInWithOAuth({
82
+ provider,
83
+ options: {
84
+ redirectTo: callbackUrl,
85
+ queryParams: {
86
+ access_type: 'offline',
87
+ prompt: 'consent',
88
+ }
89
+ }
90
+ });
91
+
92
+ // console.log(chalk.blue(`šŸ” OAuth URL: ${data?.url || 'No URL returned'}`)); // Debug only
93
+
94
+ if (error) {
95
+ console.error(chalk.red(`āŒ OAuth initiation failed: ${error.message}`));
96
+ server.close();
97
+ return false;
98
+ }
99
+
100
+ if (data.url) {
101
+ try {
102
+ await open(data.url);
103
+ } catch (openError) {
104
+ console.log(chalk.yellow('āš ļø Could not open browser automatically'));
105
+ console.log(chalk.cyan(`Please manually open: ${data.url}`));
106
+ }
107
+ }
108
+
109
+ // Wait for the callback with shorter timeout
110
+ const authResult = await Promise.race([
111
+ server.waitForCallback(),
112
+ new Promise((resolve) => {
113
+ setTimeout(() => {
114
+ resolve({ success: false, error: 'timeout', needsManualToken: true });
115
+ }, 10000); // 10 second timeout
116
+ })
117
+ ]);
118
+
119
+ // Server should already be closed by successful callback, but ensure it's closed
120
+ try {
121
+ server.close();
122
+ } catch (e) {
123
+ // Server already closed
124
+ }
125
+
126
+ if (authResult.success) {
127
+ console.log(chalk.green(`āœ… Login successful! Welcome ${authResult.user.email}`));
128
+
129
+ // Store session data
130
+ await this.storeSession(authResult.session);
131
+ return true;
132
+ } else if (authResult.needsManualToken) {
133
+ console.log(chalk.yellow('\nā° Timeout waiting for callback. Trying manual token input...'));
134
+ return await this.handleManualTokenInput();
135
+ } else {
136
+ console.error(chalk.red(`āŒ Login failed: ${authResult.error}`));
137
+ return false;
138
+ }
139
+
140
+ } catch (error) {
141
+ console.error(chalk.red(`āŒ Login error: ${error.message}`));
142
+ return false;
143
+ }
144
+ }
145
+
146
+ async createCallbackServer() {
147
+ return new Promise((resolve, reject) => {
148
+ const server = http.createServer();
149
+ let callbackPromise;
150
+ let callbackResolve;
151
+ let callbackReject;
152
+
153
+ // Create a promise that will be resolved when we get the callback
154
+ const waitForCallback = () => {
155
+ if (!callbackPromise) {
156
+ callbackPromise = new Promise((resolve, reject) => {
157
+ callbackResolve = resolve;
158
+ callbackReject = reject;
159
+ });
160
+ }
161
+ return callbackPromise;
162
+ };
163
+
164
+ server.on('request', async (req, res) => {
165
+ const parsedUrl = url.parse(req.url, true);
166
+ // console.log(chalk.blue(`šŸ” Callback received: ${req.url}`)); // Debug only
167
+
168
+ if (parsedUrl.pathname === '/callback') {
169
+ const { code, error, error_description, access_token } = parsedUrl.query;
170
+ // console.log(chalk.blue(`šŸ” OAuth callback - code: ${code ? 'present' : 'missing'}, token: ${access_token ? 'present' : 'missing'}, error: ${error || 'none'}`)); // Debug only
171
+
172
+ // Send response to browser
173
+ res.writeHead(200, { 'Content-Type': 'text/html' });
174
+
175
+ // If no query params, send HTML that can extract fragment tokens
176
+ if (!code && !access_token && !error) {
177
+ res.end(`
178
+ <html>
179
+ <head><title>OAuth Callback</title></head>
180
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
181
+ <h2>šŸ” Processing Authentication...</h2>
182
+ <p>Please wait while we complete your login.</p>
183
+ <script>
184
+ // Extract token from URL fragment and send to server
185
+ const fragment = window.location.hash.substring(1);
186
+ const params = new URLSearchParams(fragment);
187
+ const access_token = params.get('access_token');
188
+ const error = params.get('error');
189
+
190
+ if (access_token) {
191
+ // Send token to server as query parameter
192
+ fetch('/callback?access_token=' + encodeURIComponent(access_token))
193
+ .then(() => {
194
+ document.body.innerHTML = '<h2 style="color: green;">āœ… Authentication Successful!</h2><p>You can close this window and return to the terminal.</p>';
195
+ })
196
+ .catch(err => {
197
+ document.body.innerHTML = '<h2 style="color: red;">āŒ Authentication Failed</h2><p>Could not process token.</p>';
198
+ });
199
+ } else if (error) {
200
+ fetch('/callback?error=' + encodeURIComponent(error))
201
+ .then(() => {
202
+ document.body.innerHTML = '<h2 style="color: red;">āŒ Authentication Failed</h2><p>' + error + '</p>';
203
+ });
204
+ } else {
205
+ document.body.innerHTML = '<h2 style="color: orange;">āš ļø No Authentication Data</h2><p>No token or error found in URL.</p>';
206
+ }
207
+ </script>
208
+ </body>
209
+ </html>
210
+ `);
211
+ return;
212
+ }
213
+
214
+ if (error) {
215
+ res.end(`
216
+ <html>
217
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
218
+ <h2 style="color: red;">āŒ Authentication Failed</h2>
219
+ <p>${error_description || error}</p>
220
+ <p>You can close this window.</p>
221
+ </body>
222
+ </html>
223
+ `);
224
+ callbackResolve({ success: false, error: error_description || error });
225
+ } else if (access_token) {
226
+ res.end(`
227
+ <html>
228
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
229
+ <h2 style="color: green;">āœ… Authentication Successful!</h2>
230
+ <p>Token received. You can close this window and return to the terminal.</p>
231
+ </body>
232
+ </html>
233
+ `);
234
+
235
+ try {
236
+ // Use the access token directly to get user info
237
+ // console.log(chalk.blue('šŸ” Using access token from URL fragment...')); // Debug only
238
+ const tempSupabase = createClient(this.supabaseConfig.url, this.supabaseConfig.anon_key);
239
+ const { data: user, error } = await tempSupabase.auth.getUser(access_token);
240
+
241
+ if (error || !user) {
242
+ // console.log(chalk.red(`šŸ” Token verification failed: ${error?.message || 'Could not verify user'}`)); // Debug only
243
+ callbackResolve({ success: false, error: error?.message || 'Could not verify user' });
244
+ } else {
245
+ console.log(chalk.green('šŸ” Token verification successful!'));
246
+
247
+ // Create a session-like object
248
+ const mockSession = {
249
+ access_token: access_token,
250
+ user: user.user,
251
+ expires_at: Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
252
+ };
253
+
254
+ // Close server immediately on success
255
+ setTimeout(() => server.close(), 100);
256
+ callbackResolve({ success: true, session: mockSession, user: user.user });
257
+ }
258
+ } catch (tokenError) {
259
+ console.log(chalk.red(`šŸ” Token processing error: ${tokenError.message}`));
260
+ callbackResolve({ success: false, error: tokenError.message });
261
+ }
262
+ } else if (code) {
263
+ res.end(`
264
+ <html>
265
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
266
+ <h2 style="color: green;">āœ… Authentication Successful!</h2>
267
+ <p>You can close this window and return to the terminal.</p>
268
+ </body>
269
+ </html>
270
+ `);
271
+
272
+ try {
273
+ // Exchange code for session using Supabase
274
+ console.log(chalk.blue('šŸ” Exchanging code for session...'));
275
+ const { data, error } = await this.supabase.auth.exchangeCodeForSession(code);
276
+
277
+ if (error) {
278
+ console.log(chalk.red(`šŸ” Token exchange failed: ${error.message}`));
279
+ callbackResolve({ success: false, error: error.message });
280
+ } else {
281
+ console.log(chalk.green('šŸ” Token exchange successful!'));
282
+ // Close server immediately on success
283
+ setTimeout(() => server.close(), 100);
284
+ callbackResolve({ success: true, session: data.session, user: data.user });
285
+ }
286
+ } catch (exchangeError) {
287
+ console.log(chalk.red(`šŸ” Token exchange error: ${exchangeError.message}`));
288
+ callbackResolve({ success: false, error: exchangeError.message });
289
+ }
290
+ } else {
291
+ res.end(`
292
+ <html>
293
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
294
+ <h2 style="color: orange;">āš ļø Incomplete Authentication</h2>
295
+ <p>No authorization code received.</p>
296
+ </body>
297
+ </html>
298
+ `);
299
+ callbackResolve({ success: false, error: 'No authorization code received' });
300
+ }
301
+ } else {
302
+ res.writeHead(404);
303
+ res.end('Not found');
304
+ }
305
+ });
306
+
307
+ server.listen(0, 'localhost', () => {
308
+ const port = server.address().port;
309
+ // console.log(chalk.blue(`šŸ” Callback server started on http://localhost:${port}/callback`)); // Debug only
310
+ resolve({
311
+ server,
312
+ port,
313
+ close: () => server.close(),
314
+ waitForCallback
315
+ });
316
+ });
317
+
318
+ server.on('error', reject);
319
+ });
320
+ }
321
+
322
+ async storeSession(session) {
323
+ const sessionData = {
324
+ userInfo: {
325
+ email: session.user.email,
326
+ id: session.user.id
327
+ },
328
+ loginTime: Date.now(),
329
+ provider: 'supabase-oauth',
330
+ hasSupabaseSession: true,
331
+ usesSecureStorage: this.keytarAvailable
332
+ };
333
+
334
+ // Store session data in config
335
+ this.config.set('sessionData', sessionData);
336
+ this.config.set('isLoggedIn', true);
337
+
338
+ // Store JWT token securely
339
+ if (session.access_token) {
340
+ await this.storeJwtSecurely(session.user.email, session.access_token);
341
+ }
342
+
343
+ // Store refresh token securely
344
+ if (session.refresh_token) {
345
+ await this.storeRefreshTokenSecurely(session.user.email, session.refresh_token);
346
+ }
347
+ }
348
+
349
+ async storeJwtSecurely(email, jwtToken) {
350
+ if (this.keytarAvailable) {
351
+ try {
352
+ await keytar.setPassword(this.serviceName, `jwt_${email}`, jwtToken);
353
+ return true;
354
+ } catch (error) {
355
+ console.warn(chalk.yellow(`āš ļø Could not store JWT securely: ${error.message}`));
356
+ this.config.set('jwtToken', jwtToken);
357
+ return false;
358
+ }
359
+ } else {
360
+ this.config.set('jwtToken', jwtToken);
361
+ return false;
362
+ }
363
+ }
364
+
365
+ async storeRefreshTokenSecurely(email, refreshToken) {
366
+ if (this.keytarAvailable) {
367
+ try {
368
+ await keytar.setPassword(this.serviceName, `refresh_${email}`, refreshToken);
369
+ return true;
370
+ } catch (error) {
371
+ console.warn(chalk.yellow(`āš ļø Could not store refresh token securely: ${error.message}`));
372
+ this.config.set('refreshToken', refreshToken);
373
+ return false;
374
+ }
375
+ } else {
376
+ this.config.set('refreshToken', refreshToken);
377
+ return false;
378
+ }
379
+ }
380
+
381
+ async getJwtSecurely(email) {
382
+ if (this.keytarAvailable) {
383
+ try {
384
+ return await keytar.getPassword(this.serviceName, `jwt_${email}`);
385
+ } catch (error) {
386
+ return this.config.get('jwtToken');
387
+ }
388
+ } else {
389
+ return this.config.get('jwtToken');
390
+ }
391
+ }
392
+
393
+ async getRefreshTokenSecurely(email) {
394
+ if (this.keytarAvailable) {
395
+ try {
396
+ return await keytar.getPassword(this.serviceName, `refresh_${email}`);
397
+ } catch (error) {
398
+ return this.config.get('refreshToken');
399
+ }
400
+ } else {
401
+ return this.config.get('refreshToken');
402
+ }
403
+ }
404
+
405
+ isLoggedIn() {
406
+ return this.config.get('isLoggedIn', false);
407
+ }
408
+
409
+ getUserInfo() {
410
+ const sessionData = this.config.get('sessionData');
411
+ return sessionData?.userInfo || {};
412
+ }
413
+
414
+ async getApiKey() {
415
+ // For Supabase auth, we use JWT tokens instead of API keys
416
+ const sessionData = this.config.get('sessionData');
417
+ if (sessionData?.userInfo?.email) {
418
+ return await this.getJwtSecurely(sessionData.userInfo.email);
419
+ }
420
+ return null;
421
+ }
422
+
423
+ async getJwtToken() {
424
+ const sessionData = this.config.get('sessionData');
425
+ if (sessionData?.userInfo?.email) {
426
+ return await this.getJwtSecurely(sessionData.userInfo.email);
427
+ }
428
+ return null;
429
+ }
430
+
431
+ async getSelectedApiKey() {
432
+ // Get the selected/current API key (not JWT token)
433
+ const sessionData = this.config.get('sessionData');
434
+ if (sessionData?.userInfo?.email) {
435
+ if (this.keytarAvailable) {
436
+ try {
437
+ const key = await keytar.getPassword(this.serviceName, `api_${sessionData.userInfo.email}`);
438
+ if (key) {
439
+ if (process.env.AVOCAVO_DEBUG) {
440
+ console.log(chalk.green('[DEBUG] āœ… API key retrieved from OS keystore'));
441
+ }
442
+ return key;
443
+ } else {
444
+ if (process.env.AVOCAVO_DEBUG) {
445
+ console.log(chalk.yellow('[DEBUG] āš ļø No key in keystore, checking config'));
446
+ }
447
+ // Fallback to encrypted config if keytar returns null
448
+ const encrypted = this.config.get('currentApiKey_encrypted');
449
+ if (encrypted) {
450
+ if (process.env.AVOCAVO_DEBUG) {
451
+ console.log(chalk.yellow('[DEBUG] šŸ“ Using config file storage'));
452
+ }
453
+ // Decrypt the API key
454
+ return Buffer.from(encrypted, 'base64').toString('utf8');
455
+ }
456
+ }
457
+ } catch (error) {
458
+ if (process.env.AVOCAVO_DEBUG) {
459
+ console.log(chalk.red(`[DEBUG] āŒ Keytar error: ${error.message}`));
460
+ }
461
+ // Fallback to encrypted config
462
+ const encrypted = this.config.get('currentApiKey_encrypted');
463
+ if (encrypted) {
464
+ return Buffer.from(encrypted, 'base64').toString('utf8');
465
+ }
466
+ }
467
+ } else {
468
+ if (process.env.AVOCAVO_DEBUG) {
469
+ console.log(chalk.red('[DEBUG] āŒ Keytar not available'));
470
+ }
471
+ // No keytar, use encrypted config
472
+ const encrypted = this.config.get('currentApiKey_encrypted');
473
+ if (encrypted) {
474
+ return Buffer.from(encrypted, 'base64').toString('utf8');
475
+ }
476
+ }
477
+ }
478
+ return null;
479
+ }
480
+
481
+ async logout() {
482
+ // Sign out from Supabase (if client is initialized)
483
+ try {
484
+ if (this.supabase) {
485
+ await this.supabase.auth.signOut();
486
+ }
487
+ } catch (error) {
488
+ console.warn(chalk.yellow(`āš ļø Could not sign out from Supabase: ${error.message}`));
489
+ }
490
+
491
+ // Remove stored credentials
492
+ const sessionData = this.config.get('sessionData');
493
+ if (sessionData?.userInfo?.email) {
494
+ const email = sessionData.userInfo.email;
495
+
496
+ if (this.keytarAvailable) {
497
+ try {
498
+ await keytar.deletePassword(this.serviceName, `jwt_${email}`);
499
+ await keytar.deletePassword(this.serviceName, `refresh_${email}`);
500
+ await keytar.deletePassword(this.serviceName, `api_${email}`); // Delete API key too
501
+ } catch (error) {
502
+ // Continue cleanup
503
+ }
504
+ }
505
+ }
506
+
507
+ this.config.clear();
508
+ console.log(chalk.green('āœ… Successfully logged out'));
509
+
510
+ // DEBUG: Log stack trace to find duplicate logout
511
+ if (process.env.AVOCAVO_DEBUG) {
512
+ console.log('Logout called from:', new Error().stack);
513
+ }
514
+ }
515
+
516
+ // API key management methods - these will call the backend with JWT auth
517
+ async getJwtAuthHeaders() {
518
+ const jwtToken = await this.getJwtToken();
519
+ if (!jwtToken) {
520
+ throw new Error('Not logged in. Please login first.');
521
+ }
522
+
523
+ return {
524
+ 'Authorization': `Bearer ${jwtToken}`,
525
+ 'Content-Type': 'application/json'
526
+ };
527
+ }
528
+
529
+ async listApiKeys() {
530
+ try {
531
+ const headers = await this.getJwtAuthHeaders();
532
+ const response = await axios.get(`${this.baseUrl}/api/keys`, { headers, timeout: 30000 });
533
+ return response.data;
534
+ } catch (error) {
535
+ if (error.response?.status === 401) {
536
+ throw new Error('Session expired. Please login again.');
537
+ }
538
+ throw new Error(`Failed to list API keys: ${error.message}`);
539
+ }
540
+ }
541
+
542
+ async createApiKey(name = "CLI Key", description = null, environment = "development") {
543
+ try {
544
+ const headers = await this.getJwtAuthHeaders();
545
+ const data = {
546
+ key_name: name,
547
+ description: description || "Created via Supabase CLI",
548
+ environment: environment
549
+ };
550
+
551
+ const response = await axios.post(`${this.baseUrl}/api/keys`, data, { headers, timeout: 30000 });
552
+
553
+ // Auto-select the newly created key
554
+ if (response.data.success && response.data.key) {
555
+ const newKey = response.data.key;
556
+ console.log(chalk.cyan(`šŸ”„ Auto-selecting your new API key: ${newKey.key_name}`));
557
+ await this.storeApiKeySecurely(this.config.get('sessionData')?.userInfo?.email, newKey.api_key);
558
+ }
559
+
560
+ return response.data;
561
+ } catch (error) {
562
+ if (error.response?.status === 401) {
563
+ throw new Error('Session expired. Please login again.');
564
+ }
565
+ throw new Error(`Failed to create API key: ${error.message}`);
566
+ }
567
+ }
568
+
569
+ async switchApiKey(keyId) {
570
+ try {
571
+ const headers = await this.getJwtAuthHeaders();
572
+ const response = await axios.post(`${this.baseUrl}/api/keys/${keyId}/reveal`, {}, { headers, timeout: 30000 });
573
+
574
+ if (response.data.success) {
575
+ const fullApiKey = response.data.api_key;
576
+ const keyName = response.data.key_name;
577
+
578
+ // Store the selected API key
579
+ const sessionData = this.config.get('sessionData');
580
+ if (sessionData?.userInfo?.email) {
581
+ await this.storeApiKeySecurely(sessionData.userInfo.email, fullApiKey);
582
+ }
583
+
584
+ console.log(chalk.green(`āœ… Switched to API key: ${keyName}`));
585
+ return fullApiKey;
586
+ } else {
587
+ throw new Error(response.data.error || 'Failed to reveal API key');
588
+ }
589
+ } catch (error) {
590
+ throw new Error(`Failed to switch API key: ${error.message}`);
591
+ }
592
+ }
593
+
594
+ async autoSelectSingleKey() {
595
+ try {
596
+ const keysList = await this.listApiKeys();
597
+ if (keysList.keys && keysList.keys.length === 1) {
598
+ const singleKey = keysList.keys[0];
599
+ console.log(chalk.cyan(`šŸ”„ Auto-selecting your only API key: ${singleKey.key_name}`));
600
+ return await this.switchApiKey(singleKey.id);
601
+ }
602
+ return null;
603
+ } catch (error) {
604
+ return null;
605
+ }
606
+ }
607
+
608
+ async refreshApiKeyLimits(keyId) {
609
+ try {
610
+ const headers = await this.getJwtAuthHeaders();
611
+ const response = await axios.post(`${this.baseUrl}/api/keys/${keyId}/refresh-limits`, {}, { headers, timeout: 30000 });
612
+
613
+ if (response.data.success) {
614
+ console.log(chalk.green(`āœ… Updated limits: ${response.data.old_limit} → ${response.data.new_limit}`));
615
+ return response.data;
616
+ } else {
617
+ throw new Error(response.data.error || 'Failed to refresh limits');
618
+ }
619
+ } catch (error) {
620
+ throw new Error(`Failed to refresh API key limits: ${error.message}`);
621
+ }
622
+ }
623
+
624
+ async storeApiKeySecurely(email, apiKey) {
625
+ if (this.keytarAvailable) {
626
+ try {
627
+ await keytar.setPassword(this.serviceName, `api_${email}`, apiKey);
628
+ // Clear any plain text version
629
+ this.config.delete('currentApiKey');
630
+ this.config.delete('currentApiKey_encrypted');
631
+ this.config.delete('insecureStorage');
632
+
633
+ // Debug logging
634
+ if (process.env.AVOCAVO_DEBUG) {
635
+ console.log(chalk.green('[DEBUG] āœ… API key stored in OS keystore'));
636
+ }
637
+ return true;
638
+ } catch (error) {
639
+ if (process.env.AVOCAVO_DEBUG) {
640
+ console.log(chalk.red(`[DEBUG] āŒ Keytar failed: ${error.message}`));
641
+ }
642
+ // Encrypt API key before storing (basic obfuscation)
643
+ const encrypted = Buffer.from(apiKey).toString('base64');
644
+ this.config.set('currentApiKey_encrypted', encrypted);
645
+ this.config.set('insecureStorage', true);
646
+ return false;
647
+ }
648
+ } else {
649
+ if (process.env.AVOCAVO_DEBUG) {
650
+ console.log(chalk.red('[DEBUG] āŒ Keytar not available'));
651
+ }
652
+ // Encrypt API key before storing (basic obfuscation)
653
+ const encrypted = Buffer.from(apiKey).toString('base64');
654
+ this.config.set('currentApiKey_encrypted', encrypted);
655
+ this.config.set('insecureStorage', true);
656
+ return false;
657
+ }
658
+ }
659
+
660
+ async handleManualTokenInput() {
661
+ console.log(chalk.cyan('\nšŸ”§ Manual Token Input'));
662
+ console.log(chalk.yellow('If you were redirected to nutrition.avocavo.app after logging in:'));
663
+ console.log(chalk.yellow('1. Look at the URL in your browser'));
664
+ console.log(chalk.yellow('2. Find the part that says #access_token='));
665
+ console.log(chalk.yellow('3. Copy ONLY the token part (after #access_token= and before &)'));
666
+ console.log(chalk.yellow('4. Paste it below:'));
667
+
668
+ const rl = readline.createInterface({
669
+ input: process.stdin,
670
+ output: process.stdout
671
+ });
672
+
673
+ return new Promise((resolve) => {
674
+ rl.question(chalk.blue('\nPaste your access token here: '), async (token) => {
675
+ rl.close();
676
+
677
+ if (!token || token.trim().length === 0) {
678
+ console.log(chalk.red('āŒ No token provided'));
679
+ resolve(false);
680
+ return;
681
+ }
682
+
683
+ token = token.trim();
684
+
685
+ try {
686
+ // Verify token by getting user info from Supabase
687
+ const tempSupabase = createClient(this.supabaseConfig.url, this.supabaseConfig.anon_key);
688
+ const { data: user, error } = await tempSupabase.auth.getUser(token);
689
+
690
+ if (error || !user) {
691
+ console.log(chalk.red(`āŒ Invalid token: ${error?.message || 'Could not verify user'}`));
692
+ resolve(false);
693
+ return;
694
+ }
695
+
696
+ // Create a session-like object
697
+ const mockSession = {
698
+ access_token: token,
699
+ user: user.user,
700
+ expires_at: Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
701
+ };
702
+
703
+ console.log(chalk.green(`āœ… Token verified! Welcome ${user.user.email}`));
704
+
705
+ // Store session data
706
+ await this.storeSession(mockSession);
707
+ resolve(true);
708
+
709
+ } catch (error) {
710
+ console.log(chalk.red(`āŒ Token verification failed: ${error.message}`));
711
+ resolve(false);
712
+ }
713
+ });
714
+ });
715
+ }
716
+ }
717
+
718
+ module.exports = { SupabaseAuthManager };