vibecodingmachine-cli 1.0.5 → 1.0.7

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 (37) hide show
  1. package/.allnightai/REQUIREMENTS.md +11 -11
  2. package/.allnightai/temp/auto-status.json +6 -0
  3. package/.env +7 -0
  4. package/.eslintrc.js +16 -16
  5. package/README.md +85 -85
  6. package/bin/vibecodingmachine.js +274 -274
  7. package/jest.config.js +8 -8
  8. package/logs/audit/2025-11-07.jsonl +2 -2
  9. package/package.json +62 -62
  10. package/scripts/README.md +128 -128
  11. package/scripts/auto-start-wrapper.sh +92 -92
  12. package/scripts/postinstall.js +81 -81
  13. package/src/commands/auth.js +96 -96
  14. package/src/commands/auto-direct.js +1748 -1748
  15. package/src/commands/auto.js +4692 -4692
  16. package/src/commands/auto.js.bak +710 -710
  17. package/src/commands/ide.js +70 -70
  18. package/src/commands/repo.js +159 -159
  19. package/src/commands/requirements.js +161 -161
  20. package/src/commands/setup.js +91 -91
  21. package/src/commands/status.js +88 -88
  22. package/src/index.js +5 -5
  23. package/src/utils/auth.js +572 -577
  24. package/src/utils/auto-mode-ansi-ui.js +238 -238
  25. package/src/utils/auto-mode-simple-ui.js +161 -161
  26. package/src/utils/auto-mode-ui.js.bak.blessed +207 -207
  27. package/src/utils/auto-mode.js +65 -65
  28. package/src/utils/config.js +64 -64
  29. package/src/utils/interactive.js +3616 -3616
  30. package/src/utils/keyboard-handler.js +153 -152
  31. package/src/utils/logger.js +4 -4
  32. package/src/utils/persistent-header.js +116 -116
  33. package/src/utils/provider-registry.js +128 -128
  34. package/src/utils/status-card.js +120 -120
  35. package/src/utils/stdout-interceptor.js +127 -127
  36. package/tests/auto-mode.test.js +37 -37
  37. package/tests/config.test.js +34 -34
package/src/utils/auth.js CHANGED
@@ -1,577 +1,572 @@
1
- const chalk = require('chalk');
2
- const http = require('http');
3
- const crypto = require('crypto');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
7
-
8
- // AWS Cognito configuration
9
- const COGNITO_DOMAIN = process.env.COGNITO_DOMAIN || 'allnightai-auth-1763598779.auth.us-east-1.amazoncognito.com';
10
- const CLIENT_ID = process.env.COGNITO_APP_CLIENT_ID || '3tbe1i2g36uqule92iuk6snuo3'; // Public client (no secret)
11
- const PORT = 3000;
12
-
13
- // Load shared access denied HTML
14
- function getAccessDeniedHTML() {
15
- const accessDeniedPath = require.resolve('vibecodingmachine-core/src/auth/access-denied.html');
16
- return fs.readFileSync(accessDeniedPath, 'utf8');
17
- }
18
-
19
- // PKCE helper functions
20
- function generateCodeVerifier() {
21
- return crypto.randomBytes(32).toString('base64url');
22
- }
23
-
24
- function generateCodeChallenge(verifier) {
25
- return crypto.createHash('sha256').update(verifier).digest('base64url');
26
- }
27
-
28
- class CLIAuth {
29
- /**
30
- * Check if authenticated (uses shared storage)
31
- */
32
- async isAuthenticated() {
33
- return await sharedAuth.isAuthenticated();
34
- }
35
-
36
- /**
37
- * Get stored token (uses shared storage)
38
- */
39
- async getToken() {
40
- return await sharedAuth.getToken();
41
- }
42
-
43
- /**
44
- * Get user profile (uses shared storage)
45
- */
46
- async getUserProfile() {
47
- return await sharedAuth.getUserProfile();
48
- }
49
-
50
- /**
51
- * Exchange authorization code for tokens using Cognito token endpoint
52
- * @param {string} code - Authorization code
53
- * @param {string} codeVerifier - PKCE code verifier
54
- * @param {string} redirectUri - Redirect URI used in authorization request
55
- * @returns {Promise<string>} ID token
56
- */
57
- async _exchangeCodeForTokens(code, codeVerifier, redirectUri) {
58
- const https = require('https');
59
-
60
- const tokenEndpoint = `https://${COGNITO_DOMAIN}/oauth2/token`;
61
- const postData = new URLSearchParams({
62
- grant_type: 'authorization_code',
63
- client_id: CLIENT_ID,
64
- code: code,
65
- redirect_uri: redirectUri,
66
- code_verifier: codeVerifier
67
- }).toString();
68
-
69
- return new Promise((resolve, reject) => {
70
- const options = {
71
- method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/x-www-form-urlencoded',
74
- 'Content-Length': postData.length
75
- }
76
- };
77
-
78
- const req = https.request(tokenEndpoint, options, (res) => {
79
- let data = '';
80
- res.on('data', (chunk) => { data += chunk; });
81
- res.on('end', () => {
82
- if (res.statusCode === 200) {
83
- const tokens = JSON.parse(data);
84
- resolve(tokens.id_token); // Return the ID token
85
- } else {
86
- reject(new Error(`Token exchange failed: ${res.statusCode} - ${data}`));
87
- }
88
- });
89
- });
90
-
91
- req.on('error', (error) => {
92
- reject(new Error(`Token exchange request failed: ${error.message}`));
93
- });
94
-
95
- req.write(postData);
96
- req.end();
97
- });
98
- }
99
-
100
- /**
101
- * Validate JWT token with signature verification
102
- * @param {string} idToken - JWT token to validate
103
- * @throws {Error} If token is invalid
104
- */
105
- async _validateToken(idToken) {
106
- // Validate token is a string
107
- if (!idToken || typeof idToken !== 'string') {
108
- throw new Error('Token must be a non-empty string');
109
- }
110
-
111
- // Validate JWT format (must have 3 parts)
112
- const parts = idToken.split('.');
113
- if (parts.length !== 3) {
114
- throw new Error('Invalid JWT format - must have 3 parts separated by dots');
115
- }
116
-
117
- // CRITICAL: Verify JWT signature using Cognito's public keys
118
- const jwt = require('jsonwebtoken');
119
- const jwksClient = require('jwks-rsa');
120
-
121
- const region = process.env.AWS_REGION || 'us-east-1';
122
- const userPoolId = process.env.COGNITO_USER_POOL_ID || 'us-east-1_EjZ4Kbtgd';
123
- const jwksUri = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
124
-
125
- const client = jwksClient({
126
- jwksUri: jwksUri,
127
- cache: true,
128
- rateLimit: true
129
- });
130
-
131
- // Get signing key
132
- const getKey = (header, callback) => {
133
- client.getSigningKey(header.kid, (err, key) => {
134
- if (err) {
135
- callback(err);
136
- return;
137
- }
138
- const signingKey = key.getPublicKey();
139
- callback(null, signingKey);
140
- });
141
- };
142
-
143
- // Verify token signature
144
- let payload;
145
- try {
146
- payload = await new Promise((resolve, reject) => {
147
- jwt.verify(idToken, getKey, {
148
- algorithms: ['RS256'],
149
- issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,
150
- audience: CLIENT_ID
151
- }, (err, decoded) => {
152
- if (err) {
153
- reject(new Error(`Signature verification failed: ${err.message}`));
154
- } else {
155
- resolve(decoded);
156
- }
157
- });
158
- });
159
- } catch (verifyError) {
160
- throw new Error(`JWT signature verification failed: ${verifyError.message}`);
161
- }
162
-
163
- // Verify required fields exist
164
- if (!payload.exp) {
165
- throw new Error('Token missing expiration claim (exp)');
166
- }
167
- if (!payload.iss) {
168
- throw new Error('Token missing issuer claim (iss)');
169
- }
170
- if (!payload.aud) {
171
- throw new Error('Token missing audience claim (aud)');
172
- }
173
- if (!payload.token_use) {
174
- throw new Error('Token missing token_use claim');
175
- }
176
-
177
- // Verify token is not expired
178
- const now = Math.floor(Date.now() / 1000);
179
- if (payload.exp <= now) {
180
- throw new Error(`Token has expired (exp: ${payload.exp}, now: ${now})`);
181
- }
182
-
183
- // Verify token issuer matches our Cognito domain
184
- const expectedIssuer = `https://cognito-idp.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID || 'us-east-1_EjZ4Kbtgd'}`;
185
- if (payload.iss !== expectedIssuer) {
186
- throw new Error(`Invalid token issuer. Expected: ${expectedIssuer}, Got: ${payload.iss}`);
187
- }
188
-
189
- // Verify token audience matches our client ID
190
- if (payload.aud !== CLIENT_ID) {
191
- throw new Error(`Invalid token audience. Expected: ${CLIENT_ID}, Got: ${payload.aud}`);
192
- }
193
-
194
- // Verify token use is 'id'
195
- if (payload.token_use !== 'id') {
196
- throw new Error(`Invalid token use. Expected: id, Got: ${payload.token_use}`);
197
- }
198
-
199
- // Verify essential user claims exist
200
- if (!payload.sub) {
201
- throw new Error('Token missing subject claim (sub)');
202
- }
203
- if (!payload.email) {
204
- throw new Error('Token missing email claim');
205
- }
206
-
207
- return true;
208
- }
209
-
210
- /**
211
- * Check if running in VS Code Remote SSH environment
212
- */
213
- _isVSCodeRemoteSSH() {
214
- return (
215
- process.env.VSCODE_IPC_HOOK_CLI && // Running in VS Code terminal
216
- process.env.SSH_CONNECTION // In SSH session
217
- );
218
- }
219
-
220
- /**
221
- * Login via browser OAuth flow
222
- * @param {Object} options - Login options
223
- * @param {boolean} options.headless - If true, skip opening browser and provide manual instructions
224
- */
225
- async login(options = {}) {
226
- // Check if running in VS Code Remote SSH
227
- const isVSCodeRemote = this._isVSCodeRemoteSSH();
228
-
229
- // Auto-detect headless environment if not explicitly set
230
- // VS Code Remote SSH should use browser flow (not headless) because port forwarding works
231
- const isHeadless = options.headless !== undefined
232
- ? options.headless
233
- : (!process.env.DISPLAY && process.platform !== 'darwin' && process.platform !== 'win32' && !isVSCodeRemote);
234
-
235
- // Headless mode - for SSH/no-GUI environments
236
- if (isHeadless) {
237
- return this._loginHeadless();
238
- }
239
-
240
- // VS Code Remote SSH - use optimized flow with instructions
241
- if (isVSCodeRemote && !options.headless) {
242
- console.log(chalk.cyan('\nšŸ” VS Code Remote SSH Detected!\n'));
243
- console.log(chalk.gray('VS Code will automatically forward port 3000 to your laptop.\n'));
244
- console.log(chalk.yellow('If the browser doesn\'t open automatically:'));
245
- console.log(chalk.gray(' 1. VS Code will show "Port 3000 is now available"'));
246
- console.log(chalk.gray(' 2. Click "Open in Browser" in the notification\n'));
247
- }
248
-
249
- // Standard browser-based login
250
- return new Promise((resolve, reject) => {
251
- let serverClosed = false;
252
-
253
- // Generate PKCE code verifier and challenge
254
- const codeVerifier = generateCodeVerifier();
255
- const codeChallenge = generateCodeChallenge(codeVerifier);
256
-
257
- // Create local server to receive callback
258
- const server = http.createServer(async (req, res) => {
259
- const url = new URL(req.url, `http://localhost:${PORT}`);
260
-
261
- if (url.pathname === '/callback') {
262
- // Extract authorization code from query params
263
- const code = url.searchParams.get('code');
264
-
265
- if (code) {
266
- try {
267
- // Exchange code for tokens
268
- console.log(chalk.gray('Exchanging authorization code for tokens...'));
269
- const idToken = await this._exchangeCodeForTokens(
270
- code,
271
- codeVerifier,
272
- `http://localhost:${PORT}/callback`
273
- );
274
-
275
- // Validate token (signature + claims)
276
- console.log(chalk.gray('Validating token...'));
277
- await this._validateToken(idToken);
278
-
279
- // Save token
280
- await sharedAuth.saveToken(idToken);
281
-
282
- // Show success page
283
- res.writeHead(200, {
284
- 'Content-Type': 'text/html',
285
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
286
- 'Pragma': 'no-cache',
287
- 'Expires': '0'
288
- });
289
- res.end(`
290
- <!DOCTYPE html>
291
- <html>
292
- <head>
293
- <meta charset="UTF-8">
294
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
295
- <style>
296
- * {
297
- margin: 0;
298
- padding: 0;
299
- box-sizing: border-box;
300
- }
301
- html, body {
302
- width: 100%;
303
- height: 100%;
304
- overflow: hidden;
305
- position: fixed;
306
- }
307
- body {
308
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
309
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
310
- color: white;
311
- }
312
- .wrapper {
313
- width: 100%;
314
- height: 100%;
315
- display: flex;
316
- align-items: center;
317
- justify-content: center;
318
- padding: 20px;
319
- }
320
- .container {
321
- background: white;
322
- color: #333;
323
- padding: 40px;
324
- border-radius: 12px;
325
- box-shadow: 0 8px 32px rgba(0,0,0,0.3);
326
- width: 500px;
327
- max-width: 100%;
328
- text-align: center;
329
- }
330
- h1 { margin: 0 0 20px 0; color: #10b981; }
331
- p { margin: 10px 0; color: #666; }
332
- .success-icon { font-size: 48px; margin-bottom: 10px; }
333
- .info-box {
334
- margin-top: 30px;
335
- padding: 16px;
336
- background: #f3f4f6;
337
- border-radius: 8px;
338
- border-left: 4px solid #10b981;
339
- }
340
- .info-title {
341
- font-weight: 600;
342
- margin-bottom: 8px;
343
- color: #374151;
344
- }
345
- code {
346
- background: #1f2937;
347
- color: #10b981;
348
- padding: 3px 8px;
349
- border-radius: 4px;
350
- font-family: 'Monaco', 'Courier New', monospace;
351
- display: inline-block;
352
- margin: 4px 0;
353
- }
354
- .command-list {
355
- margin-top: 12px;
356
- }
357
- </style>
358
- </head>
359
- <body>
360
- <div class="wrapper">
361
- <div class="container">
362
- <h1>Authentication Successful!</h1>
363
- <p>You are now logged in to Vibe Coding Machine.</p>
364
- <p>You can close this window and return to the terminal.</p>
365
-
366
- <div class="info-box">
367
- <div class="info-title">Available Commands:</div>
368
- <div class="command-list">
369
- <code>vcm</code> - Start interactive mode<br>
370
- <code>vcm auth:status</code> - Check authentication status<br>
371
- <code>vcm auth:logout</code> - Logout
372
- </div>
373
- </div>
374
- </div>
375
- </div>
376
- </body>
377
- </html>
378
- `);
379
-
380
- // Close server and resolve
381
- if (!serverClosed) {
382
- serverClosed = true;
383
- server.close();
384
- console.log(chalk.green('āœ“ Authentication successful!\n'));
385
- resolve(idToken);
386
- }
387
- } catch (error) {
388
- // Token exchange or validation failed
389
- console.error(chalk.red('\nāœ— Authentication failed:'), error.message);
390
-
391
- // Load shared access denied HTML
392
- res.writeHead(400, { 'Content-Type': 'text/html' });
393
- res.end(getAccessDeniedHTML());
394
-
395
- if (!serverClosed) {
396
- serverClosed = true;
397
- server.close();
398
- reject(error);
399
- }
400
- }
401
- } else {
402
- res.writeHead(400, { 'Content-Type': 'text/plain' });
403
- res.end('No authorization code received');
404
- }
405
- }
406
- });
407
-
408
- server.listen(PORT, async () => {
409
- // Build Cognito OAuth URL with PKCE - force Google account picker every time
410
- const authUrl = `https://${COGNITO_DOMAIN}/oauth2/authorize?` +
411
- `client_id=${CLIENT_ID}&` +
412
- `response_type=code&` +
413
- `scope=email+openid+profile&` +
414
- `redirect_uri=http://localhost:${PORT}/callback&` +
415
- `code_challenge=${codeChallenge}&` +
416
- `code_challenge_method=S256&` +
417
- `identity_provider=Google&` +
418
- `prompt=select_account`;
419
-
420
- // Open browser - use VS Code command if in Remote SSH, otherwise use standard open
421
- if (this._isVSCodeRemoteSSH()) {
422
- const { exec } = require('child_process');
423
-
424
- // Try using VS Code's code command to open URL on local machine
425
- exec(`code --open-url "${authUrl}"`, (error) => {
426
- if (error) {
427
- console.log(chalk.yellow('\nāš ļø Could not open browser automatically.'));
428
- console.log(chalk.gray('Please manually open this URL:\n'));
429
- console.log(chalk.blue(` ${authUrl}\n`));
430
- }
431
- });
432
- } else {
433
- // Standard open for local or GUI environments
434
- const open = (await import('open')).default;
435
- try {
436
- await open(authUrl);
437
- console.log(chalk.gray('\nIf the browser did not open automatically, please manually open:'));
438
- console.log(chalk.blue(` ${authUrl}\n`));
439
- } catch (error) {
440
- console.log(chalk.yellow('\nāš ļø Could not open browser automatically.'));
441
- console.log(chalk.gray('Please manually open this URL:\n'));
442
- console.log(chalk.blue(` ${authUrl}\n`));
443
- }
444
- }
445
- });
446
-
447
- // Timeout after 5 minutes
448
- setTimeout(() => {
449
- if (!serverClosed) {
450
- serverClosed = true;
451
- server.close();
452
- reject(new Error('Authentication timeout'));
453
- }
454
- }, 5 * 60 * 1000);
455
- });
456
- }
457
-
458
- /**
459
- * Headless login - for SSH/no-GUI environments
460
- * User manually opens URL and pastes back the callback URL
461
- */
462
- async _loginHeadless() {
463
- const inquirer = require('inquirer');
464
-
465
- // Generate PKCE code verifier and challenge
466
- const codeVerifier = generateCodeVerifier();
467
- const codeChallenge = generateCodeChallenge(codeVerifier);
468
-
469
- // Build Cognito OAuth URL with PKCE
470
- const authUrl = `https://${COGNITO_DOMAIN}/oauth2/authorize?` +
471
- `client_id=${CLIENT_ID}&` +
472
- `response_type=code&` +
473
- `scope=email+openid+profile&` +
474
- `redirect_uri=http://localhost:3000/callback&` +
475
- `code_challenge=${codeChallenge}&` +
476
- `code_challenge_method=S256&` +
477
- `identity_provider=Google&` +
478
- `prompt=select_account`;
479
-
480
- console.log(chalk.cyan('\nšŸ” Headless Authentication Mode\n'));
481
- console.log(chalk.gray('You are in a headless/SSH environment.\n'));
482
- console.log(chalk.white('Please follow these steps:\n'));
483
- console.log(chalk.yellow('1. Open this URL in a browser on any device (Cmd-Double-Click might work):'));
484
- console.log(chalk.blue(`\n ${authUrl}\n`));
485
- console.log(chalk.yellow('2. Sign in with Google (may happen automatically if already signed in)'));
486
- console.log(chalk.yellow('3. Browser will redirect to localhost and show "connection refused" error'));
487
- console.log(chalk.yellow(' ' + chalk.gray('(This is expected - localhost is not accessible from your device)')));
488
- console.log(chalk.yellow('4. Copy the FULL URL from your browser address bar'));
489
- console.log(chalk.yellow(' ' + chalk.gray('(It will start with: http://localhost:3000/callback?code=...)')));
490
- console.log(chalk.yellow('5. Paste it below\n'));
491
-
492
- const { callbackUrl } = await inquirer.prompt([{
493
- type: 'input',
494
- name: 'callbackUrl',
495
- message: 'Paste the callback URL here:',
496
- validate: (input) => {
497
- if (!input || input.trim() === '') {
498
- return 'Please provide the callback URL';
499
- }
500
- if (!input.includes('code=')) {
501
- return 'Invalid URL - should contain "code=" parameter';
502
- }
503
- if (!input.includes('localhost:3000/callback')) {
504
- return 'Invalid callback URL - should be from localhost:3000/callback';
505
- }
506
- return true;
507
- }
508
- }]);
509
-
510
- try {
511
- // Extract authorization code from URL query params
512
- const urlObj = new URL(callbackUrl.trim());
513
- const code = urlObj.searchParams.get('code');
514
-
515
- if (!code) {
516
- throw new Error('No authorization code found in URL');
517
- }
518
-
519
- // Exchange code for tokens
520
- console.log(chalk.gray('Exchanging authorization code for tokens...'));
521
- const idToken = await this._exchangeCodeForTokens(
522
- code,
523
- codeVerifier,
524
- 'http://localhost:3000/callback'
525
- );
526
-
527
- // Validate JWT token
528
- console.log(chalk.gray('Validating token...'));
529
- try {
530
- await this._validateToken(idToken);
531
- console.log(chalk.gray('āœ“ Token validation and signature verification passed'));
532
- } catch (validationError) {
533
- console.error(chalk.red('\nāœ— Token validation failed:'), validationError.message);
534
- throw new Error(`Invalid token: ${validationError.message}`);
535
- }
536
-
537
- // Save token using shared storage (only if validation passed)
538
- await sharedAuth.saveToken(idToken);
539
-
540
- console.log(chalk.green('\nāœ“ Authentication successful!'));
541
- return idToken;
542
- } catch (error) {
543
- console.error(chalk.red('\nāœ— Authentication failed:'), error.message);
544
- throw error;
545
- }
546
- }
547
-
548
- /**
549
- * Logout (uses shared storage)
550
- */
551
- async logout() {
552
- return await sharedAuth.logout();
553
- }
554
-
555
- /**
556
- * Check if can run auto mode (uses shared storage)
557
- */
558
- async canRunAutoMode() {
559
- return await sharedAuth.canRunAutoMode();
560
- }
561
-
562
- /**
563
- * Log iteration (uses shared storage)
564
- */
565
- async logIteration() {
566
- return await sharedAuth.logIteration();
567
- }
568
-
569
- /**
570
- * Activate license key (uses shared storage)
571
- */
572
- async activateLicense(licenseKey) {
573
- return await sharedAuth.activateLicense(licenseKey);
574
- }
575
- }
576
-
577
- module.exports = new CLIAuth();
1
+ const chalk = require('chalk');
2
+ const http = require('http');
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
7
+
8
+ // AWS Cognito configuration
9
+ const COGNITO_DOMAIN = process.env.COGNITO_DOMAIN || 'allnightai-auth-1763598779.auth.us-east-1.amazoncognito.com';
10
+ const CLIENT_ID = process.env.COGNITO_APP_CLIENT_ID || '3tbe1i2g36uqule92iuk6snuo3'; // Public client (no secret)
11
+ const PORT = 3000;
12
+
13
+ // Load shared access denied HTML
14
+ function getAccessDeniedHTML() {
15
+ const accessDeniedPath = require.resolve('vibecodingmachine-core/src/auth/access-denied.html');
16
+ return fs.readFileSync(accessDeniedPath, 'utf8');
17
+ }
18
+
19
+ // PKCE helper functions
20
+ function generateCodeVerifier() {
21
+ return crypto.randomBytes(32).toString('base64url');
22
+ }
23
+
24
+ function generateCodeChallenge(verifier) {
25
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
26
+ }
27
+
28
+ class CLIAuth {
29
+ /**
30
+ * Check if authenticated (uses shared storage)
31
+ */
32
+ async isAuthenticated() {
33
+ return await sharedAuth.isAuthenticated();
34
+ }
35
+
36
+ /**
37
+ * Get stored token (uses shared storage)
38
+ */
39
+ async getToken() {
40
+ return await sharedAuth.getToken();
41
+ }
42
+
43
+ /**
44
+ * Get user profile (uses shared storage)
45
+ */
46
+ async getUserProfile() {
47
+ return await sharedAuth.getUserProfile();
48
+ }
49
+
50
+ /**
51
+ * Exchange authorization code for tokens using Cognito token endpoint
52
+ * @param {string} code - Authorization code
53
+ * @param {string} codeVerifier - PKCE code verifier
54
+ * @param {string} redirectUri - Redirect URI used in authorization request
55
+ * @returns {Promise<string>} ID token
56
+ */
57
+ async _exchangeCodeForTokens(code, codeVerifier, redirectUri) {
58
+ const https = require('https');
59
+
60
+ const tokenEndpoint = `https://${COGNITO_DOMAIN}/oauth2/token`;
61
+ const postData = new URLSearchParams({
62
+ grant_type: 'authorization_code',
63
+ client_id: CLIENT_ID,
64
+ code: code,
65
+ redirect_uri: redirectUri,
66
+ code_verifier: codeVerifier
67
+ }).toString();
68
+
69
+ return new Promise((resolve, reject) => {
70
+ const options = {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/x-www-form-urlencoded',
74
+ 'Content-Length': postData.length
75
+ }
76
+ };
77
+
78
+ const req = https.request(tokenEndpoint, options, (res) => {
79
+ let data = '';
80
+ res.on('data', (chunk) => { data += chunk; });
81
+ res.on('end', () => {
82
+ if (res.statusCode === 200) {
83
+ const tokens = JSON.parse(data);
84
+ resolve(tokens.id_token); // Return the ID token
85
+ } else {
86
+ reject(new Error(`Token exchange failed: ${res.statusCode} - ${data}`));
87
+ }
88
+ });
89
+ });
90
+
91
+ req.on('error', (error) => {
92
+ reject(new Error(`Token exchange request failed: ${error.message}`));
93
+ });
94
+
95
+ req.write(postData);
96
+ req.end();
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Validate JWT token with signature verification
102
+ * @param {string} idToken - JWT token to validate
103
+ * @throws {Error} If token is invalid
104
+ */
105
+ async _validateToken(idToken) {
106
+ // Validate token is a string
107
+ if (!idToken || typeof idToken !== 'string') {
108
+ throw new Error('Token must be a non-empty string');
109
+ }
110
+
111
+ // Validate JWT format (must have 3 parts)
112
+ const parts = idToken.split('.');
113
+ if (parts.length !== 3) {
114
+ throw new Error('Invalid JWT format - must have 3 parts separated by dots');
115
+ }
116
+
117
+ // CRITICAL: Verify JWT signature using Cognito's public keys
118
+ const jwt = require('jsonwebtoken');
119
+ const jwksClient = require('jwks-rsa');
120
+
121
+ const region = process.env.AWS_REGION || 'us-east-1';
122
+ const userPoolId = process.env.COGNITO_USER_POOL_ID || 'us-east-1_EjZ4Kbtgd';
123
+ const jwksUri = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
124
+
125
+ const client = jwksClient({
126
+ jwksUri: jwksUri,
127
+ cache: true,
128
+ rateLimit: true
129
+ });
130
+
131
+ // Get signing key
132
+ const getKey = (header, callback) => {
133
+ client.getSigningKey(header.kid, (err, key) => {
134
+ if (err) {
135
+ callback(err);
136
+ return;
137
+ }
138
+ const signingKey = key.getPublicKey();
139
+ callback(null, signingKey);
140
+ });
141
+ };
142
+
143
+ // Verify token signature
144
+ let payload;
145
+ try {
146
+ payload = await new Promise((resolve, reject) => {
147
+ jwt.verify(idToken, getKey, {
148
+ algorithms: ['RS256'],
149
+ issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,
150
+ audience: CLIENT_ID
151
+ }, (err, decoded) => {
152
+ if (err) {
153
+ reject(new Error(`Signature verification failed: ${err.message}`));
154
+ } else {
155
+ resolve(decoded);
156
+ }
157
+ });
158
+ });
159
+ } catch (verifyError) {
160
+ throw new Error(`JWT signature verification failed: ${verifyError.message}`);
161
+ }
162
+
163
+ // Verify required fields exist
164
+ if (!payload.exp) {
165
+ throw new Error('Token missing expiration claim (exp)');
166
+ }
167
+ if (!payload.iss) {
168
+ throw new Error('Token missing issuer claim (iss)');
169
+ }
170
+ if (!payload.aud) {
171
+ throw new Error('Token missing audience claim (aud)');
172
+ }
173
+ if (!payload.token_use) {
174
+ throw new Error('Token missing token_use claim');
175
+ }
176
+
177
+ // Verify token is not expired
178
+ const now = Math.floor(Date.now() / 1000);
179
+ if (payload.exp <= now) {
180
+ throw new Error(`Token has expired (exp: ${payload.exp}, now: ${now})`);
181
+ }
182
+
183
+ // Verify token issuer matches our Cognito domain
184
+ const expectedIssuer = `https://cognito-idp.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID || 'us-east-1_EjZ4Kbtgd'}`;
185
+ if (payload.iss !== expectedIssuer) {
186
+ throw new Error(`Invalid token issuer. Expected: ${expectedIssuer}, Got: ${payload.iss}`);
187
+ }
188
+
189
+ // Verify token audience matches our client ID
190
+ if (payload.aud !== CLIENT_ID) {
191
+ throw new Error(`Invalid token audience. Expected: ${CLIENT_ID}, Got: ${payload.aud}`);
192
+ }
193
+
194
+ // Verify token use is 'id'
195
+ if (payload.token_use !== 'id') {
196
+ throw new Error(`Invalid token use. Expected: id, Got: ${payload.token_use}`);
197
+ }
198
+
199
+ // Verify essential user claims exist
200
+ if (!payload.sub) {
201
+ throw new Error('Token missing subject claim (sub)');
202
+ }
203
+ if (!payload.email) {
204
+ throw new Error('Token missing email claim');
205
+ }
206
+
207
+ return true;
208
+ }
209
+
210
+ /**
211
+ * Check if running in VS Code Remote SSH environment
212
+ */
213
+ _isVSCodeRemoteSSH() {
214
+ return (
215
+ process.env.VSCODE_IPC_HOOK_CLI && // Running in VS Code terminal
216
+ process.env.SSH_CONNECTION // In SSH session
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Login via browser OAuth flow
222
+ * @param {Object} options - Login options
223
+ * @param {boolean} options.headless - If true, skip opening browser and provide manual instructions
224
+ */
225
+ async login(options = {}) {
226
+ // Check if running in VS Code Remote SSH
227
+ const isVSCodeRemote = this._isVSCodeRemoteSSH();
228
+
229
+ // Auto-detect headless environment if not explicitly set
230
+ // VS Code Remote SSH should use browser flow (not headless) because port forwarding works
231
+ const isHeadless = options.headless !== undefined
232
+ ? options.headless
233
+ : (!process.env.DISPLAY && process.platform !== 'darwin' && process.platform !== 'win32' && !isVSCodeRemote);
234
+
235
+ // Headless mode - for SSH/no-GUI environments
236
+ if (isHeadless) {
237
+ return this._loginHeadless();
238
+ }
239
+
240
+ // VS Code Remote SSH - use optimized flow with instructions
241
+ if (isVSCodeRemote && !options.headless) {
242
+ console.log(chalk.cyan('\nšŸ” VS Code Remote SSH Detected!\n'));
243
+ console.log(chalk.yellow('āš ļø IMPORTANT: VS Code will show "Port 3000 is now available"'));
244
+ console.log(chalk.yellow(' → DO NOT click that notification!'));
245
+ console.log(chalk.yellow(' → Instead, Ctrl+Click the authentication URL shown below\n'));
246
+ }
247
+
248
+ // Standard browser-based login
249
+ return new Promise((resolve, reject) => {
250
+ let serverClosed = false;
251
+
252
+ // Generate PKCE code verifier and challenge
253
+ const codeVerifier = generateCodeVerifier();
254
+ const codeChallenge = generateCodeChallenge(codeVerifier);
255
+
256
+ // Create local server to receive callback
257
+ const server = http.createServer(async (req, res) => {
258
+ const url = new URL(req.url, `http://localhost:${PORT}`);
259
+
260
+ if (url.pathname === '/callback') {
261
+ // Extract authorization code from query params
262
+ const code = url.searchParams.get('code');
263
+
264
+ if (code) {
265
+ try {
266
+ // Exchange code for tokens
267
+ console.log(chalk.gray('Exchanging authorization code for tokens...'));
268
+ const idToken = await this._exchangeCodeForTokens(
269
+ code,
270
+ codeVerifier,
271
+ `http://localhost:${PORT}/callback`
272
+ );
273
+
274
+ // Validate token (signature + claims)
275
+ console.log(chalk.gray('Validating token...'));
276
+ await this._validateToken(idToken);
277
+
278
+ // Save token
279
+ await sharedAuth.saveToken(idToken);
280
+
281
+ // Show success page
282
+ res.writeHead(200, {
283
+ 'Content-Type': 'text/html',
284
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
285
+ 'Pragma': 'no-cache',
286
+ 'Expires': '0'
287
+ });
288
+ res.end(`
289
+ <!DOCTYPE html>
290
+ <html>
291
+ <head>
292
+ <meta charset="UTF-8">
293
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
294
+ <style>
295
+ * {
296
+ margin: 0;
297
+ padding: 0;
298
+ box-sizing: border-box;
299
+ }
300
+ html, body {
301
+ width: 100%;
302
+ height: 100%;
303
+ overflow: hidden;
304
+ position: fixed;
305
+ }
306
+ body {
307
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
308
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
309
+ color: white;
310
+ }
311
+ .wrapper {
312
+ width: 100%;
313
+ height: 100%;
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: center;
317
+ padding: 20px;
318
+ }
319
+ .container {
320
+ background: white;
321
+ color: #333;
322
+ padding: 40px;
323
+ border-radius: 12px;
324
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
325
+ width: 500px;
326
+ max-width: 100%;
327
+ text-align: center;
328
+ }
329
+ h1 { margin: 0 0 20px 0; color: #10b981; }
330
+ p { margin: 10px 0; color: #666; }
331
+ .success-icon { font-size: 48px; margin-bottom: 10px; }
332
+ .info-box {
333
+ margin-top: 30px;
334
+ padding: 16px;
335
+ background: #f3f4f6;
336
+ border-radius: 8px;
337
+ border-left: 4px solid #10b981;
338
+ }
339
+ .info-title {
340
+ font-weight: 600;
341
+ margin-bottom: 8px;
342
+ color: #374151;
343
+ }
344
+ code {
345
+ background: #1f2937;
346
+ color: #10b981;
347
+ padding: 3px 8px;
348
+ border-radius: 4px;
349
+ font-family: 'Monaco', 'Courier New', monospace;
350
+ display: inline-block;
351
+ margin: 4px 0;
352
+ }
353
+ .command-list {
354
+ margin-top: 12px;
355
+ }
356
+ </style>
357
+ </head>
358
+ <body>
359
+ <div class="wrapper">
360
+ <div class="container">
361
+ <h1>Authentication Successful!</h1>
362
+ <p>You are now logged in to Vibe Coding Machine.</p>
363
+ <p>You can close this window and return to the terminal.</p>
364
+
365
+ <div class="info-box">
366
+ <div class="info-title">Available Commands:</div>
367
+ <div class="command-list">
368
+ <code>vcm</code> - Start interactive mode<br>
369
+ <code>vcm auth:status</code> - Check authentication status<br>
370
+ <code>vcm auth:logout</code> - Logout
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </body>
376
+ </html>
377
+ `);
378
+
379
+ // Close server and resolve
380
+ if (!serverClosed) {
381
+ serverClosed = true;
382
+ server.close();
383
+ console.log(chalk.green('āœ“ Authentication successful!\n'));
384
+ resolve(idToken);
385
+ }
386
+ } catch (error) {
387
+ // Token exchange or validation failed
388
+ console.error(chalk.red('\nāœ— Authentication failed:'), error.message);
389
+
390
+ // Load shared access denied HTML
391
+ res.writeHead(400, { 'Content-Type': 'text/html' });
392
+ res.end(getAccessDeniedHTML());
393
+
394
+ if (!serverClosed) {
395
+ serverClosed = true;
396
+ server.close();
397
+ reject(error);
398
+ }
399
+ }
400
+ } else {
401
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
402
+ res.end('No authorization code received');
403
+ }
404
+ }
405
+ });
406
+
407
+ server.listen(PORT, async () => {
408
+ // Build Cognito OAuth URL with PKCE - force Google account picker every time
409
+ const authUrl = `https://${COGNITO_DOMAIN}/oauth2/authorize?` +
410
+ `client_id=${CLIENT_ID}&` +
411
+ `response_type=code&` +
412
+ `scope=email+openid+profile&` +
413
+ `redirect_uri=http://localhost:${PORT}/callback&` +
414
+ `code_challenge=${codeChallenge}&` +
415
+ `code_challenge_method=S256&` +
416
+ `identity_provider=Google&` +
417
+ `prompt=select_account`;
418
+
419
+ // Open browser - VS Code Remote SSH users should click the URL
420
+ if (this._isVSCodeRemoteSSH()) {
421
+ // For VS Code Remote SSH, just show the URL prominently
422
+ // Port forwarding will automatically work when they click it
423
+ console.log(chalk.green('šŸ‘‰ Click this URL to login:\n'));
424
+ console.log(chalk.blue.underline(` ${authUrl}\n`));
425
+ console.log(chalk.gray('(Ctrl+Click or Cmd+Click to open in your browser)\n'));
426
+ } else {
427
+ // Standard open for local or GUI environments
428
+ const open = (await import('open')).default;
429
+ try {
430
+ await open(authUrl);
431
+ console.log(chalk.gray('\nOpening browser for authentication...'));
432
+ console.log(chalk.gray('If it doesn\'t open, click this URL:\n'));
433
+ console.log(chalk.blue.underline(` ${authUrl}\n`));
434
+ } catch (error) {
435
+ console.log(chalk.yellow('\nāš ļø Could not open browser automatically.'));
436
+ console.log(chalk.gray('Please click this URL to login:\n'));
437
+ console.log(chalk.blue.underline(` ${authUrl}\n`));
438
+ }
439
+ }
440
+ });
441
+
442
+ // Timeout after 5 minutes
443
+ setTimeout(() => {
444
+ if (!serverClosed) {
445
+ serverClosed = true;
446
+ server.close();
447
+ reject(new Error('Authentication timeout'));
448
+ }
449
+ }, 5 * 60 * 1000);
450
+ });
451
+ }
452
+
453
+ /**
454
+ * Headless login - for SSH/no-GUI environments
455
+ * User manually opens URL and pastes back the callback URL
456
+ */
457
+ async _loginHeadless() {
458
+ const inquirer = require('inquirer');
459
+
460
+ // Generate PKCE code verifier and challenge
461
+ const codeVerifier = generateCodeVerifier();
462
+ const codeChallenge = generateCodeChallenge(codeVerifier);
463
+
464
+ // Build Cognito OAuth URL with PKCE
465
+ const authUrl = `https://${COGNITO_DOMAIN}/oauth2/authorize?` +
466
+ `client_id=${CLIENT_ID}&` +
467
+ `response_type=code&` +
468
+ `scope=email+openid+profile&` +
469
+ `redirect_uri=http://localhost:3000/callback&` +
470
+ `code_challenge=${codeChallenge}&` +
471
+ `code_challenge_method=S256&` +
472
+ `identity_provider=Google&` +
473
+ `prompt=select_account`;
474
+
475
+ console.log(chalk.cyan('\nšŸ” Headless Authentication Mode\n'));
476
+ console.log(chalk.gray('You are in a headless/SSH environment.\n'));
477
+ console.log(chalk.white('Please follow these steps:\n'));
478
+ console.log(chalk.yellow('1. Open this URL in a browser on any device (Cmd-Double-Click might work):'));
479
+ console.log(chalk.blue(`\n ${authUrl}\n`));
480
+ console.log(chalk.yellow('2. Sign in with Google (may happen automatically if already signed in)'));
481
+ console.log(chalk.yellow('3. Browser will redirect to localhost and show "connection refused" error'));
482
+ console.log(chalk.yellow(' ' + chalk.gray('(This is expected - localhost is not accessible from your device)')));
483
+ console.log(chalk.yellow('4. Copy the FULL URL from your browser address bar'));
484
+ console.log(chalk.yellow(' ' + chalk.gray('(It will start with: http://localhost:3000/callback?code=...)')));
485
+ console.log(chalk.yellow('5. Paste it below\n'));
486
+
487
+ const { callbackUrl } = await inquirer.prompt([{
488
+ type: 'input',
489
+ name: 'callbackUrl',
490
+ message: 'Paste the callback URL here:',
491
+ validate: (input) => {
492
+ if (!input || input.trim() === '') {
493
+ return 'Please provide the callback URL';
494
+ }
495
+ if (!input.includes('code=')) {
496
+ return 'Invalid URL - should contain "code=" parameter';
497
+ }
498
+ if (!input.includes('localhost:3000/callback')) {
499
+ return 'Invalid callback URL - should be from localhost:3000/callback';
500
+ }
501
+ return true;
502
+ }
503
+ }]);
504
+
505
+ try {
506
+ // Extract authorization code from URL query params
507
+ const urlObj = new URL(callbackUrl.trim());
508
+ const code = urlObj.searchParams.get('code');
509
+
510
+ if (!code) {
511
+ throw new Error('No authorization code found in URL');
512
+ }
513
+
514
+ // Exchange code for tokens
515
+ console.log(chalk.gray('Exchanging authorization code for tokens...'));
516
+ const idToken = await this._exchangeCodeForTokens(
517
+ code,
518
+ codeVerifier,
519
+ 'http://localhost:3000/callback'
520
+ );
521
+
522
+ // Validate JWT token
523
+ console.log(chalk.gray('Validating token...'));
524
+ try {
525
+ await this._validateToken(idToken);
526
+ console.log(chalk.gray('āœ“ Token validation and signature verification passed'));
527
+ } catch (validationError) {
528
+ console.error(chalk.red('\nāœ— Token validation failed:'), validationError.message);
529
+ throw new Error(`Invalid token: ${validationError.message}`);
530
+ }
531
+
532
+ // Save token using shared storage (only if validation passed)
533
+ await sharedAuth.saveToken(idToken);
534
+
535
+ console.log(chalk.green('\nāœ“ Authentication successful!'));
536
+ return idToken;
537
+ } catch (error) {
538
+ console.error(chalk.red('\nāœ— Authentication failed:'), error.message);
539
+ throw error;
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Logout (uses shared storage)
545
+ */
546
+ async logout() {
547
+ return await sharedAuth.logout();
548
+ }
549
+
550
+ /**
551
+ * Check if can run auto mode (uses shared storage)
552
+ */
553
+ async canRunAutoMode() {
554
+ return await sharedAuth.canRunAutoMode();
555
+ }
556
+
557
+ /**
558
+ * Log iteration (uses shared storage)
559
+ */
560
+ async logIteration() {
561
+ return await sharedAuth.logIteration();
562
+ }
563
+
564
+ /**
565
+ * Activate license key (uses shared storage)
566
+ */
567
+ async activateLicense(licenseKey) {
568
+ return await sharedAuth.activateLicense(licenseKey);
569
+ }
570
+ }
571
+
572
+ module.exports = new CLIAuth();