vibecodingmachine-cli 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allnightai/REQUIREMENTS.md +11 -11
- package/.eslintrc.js +16 -16
- package/README.md +85 -85
- package/bin/vibecodingmachine.js +274 -274
- package/jest.config.js +8 -8
- package/logs/audit/2025-11-07.jsonl +2 -2
- package/package.json +62 -66
- package/scripts/README.md +128 -128
- package/scripts/auto-start-wrapper.sh +92 -92
- package/scripts/postinstall.js +81 -81
- package/src/commands/auth.js +96 -96
- package/src/commands/auto-direct.js +1748 -1748
- package/src/commands/auto.js +4692 -4692
- package/src/commands/auto.js.bak +710 -710
- package/src/commands/ide.js +70 -70
- package/src/commands/repo.js +159 -159
- package/src/commands/requirements.js +161 -161
- package/src/commands/setup.js +91 -91
- package/src/commands/status.js +88 -88
- package/src/index.js +5 -5
- package/src/utils/auth.js +577 -577
- package/src/utils/auto-mode-ansi-ui.js +238 -238
- package/src/utils/auto-mode-simple-ui.js +161 -161
- package/src/utils/auto-mode-ui.js.bak.blessed +207 -207
- package/src/utils/auto-mode.js +65 -65
- package/src/utils/config.js +64 -64
- package/src/utils/interactive.js +3616 -3616
- package/src/utils/keyboard-handler.js +152 -152
- package/src/utils/logger.js +4 -4
- package/src/utils/persistent-header.js +116 -116
- package/src/utils/provider-registry.js +128 -128
- package/src/utils/status-card.js +120 -120
- package/src/utils/stdout-interceptor.js +127 -127
- package/tests/auto-mode.test.js +37 -37
- package/tests/config.test.js +34 -34
- package/.allnightai/temp/auto-status.json +0 -6
- package/.env +0 -7
package/src/utils/auth.js
CHANGED
|
@@ -1,577 +1,577 @@
|
|
|
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.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();
|