merge: integrate upstream/main (v1.2.15) into feature/webui
- Resolved conflict in src/constants.js: kept config-driven approach - Adopted upstream 10-second cooldown default - Added MAX_EMPTY_RESPONSE_RETRIES constant from upstream - Incorporated new test files and GitHub issue templates
This commit is contained in:
@@ -58,6 +58,56 @@ export function getAuthorizationUrl(customRedirectUri = null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract authorization code and state from user input.
|
||||
* User can paste either:
|
||||
* - Full callback URL: http://localhost:51121/oauth-callback?code=xxx&state=xxx
|
||||
* - Just the code parameter: 4/0xxx...
|
||||
*
|
||||
* @param {string} input - User input (URL or code)
|
||||
* @returns {{code: string, state: string|null}} Extracted code and optional state
|
||||
*/
|
||||
export function extractCodeFromInput(input) {
|
||||
if (!input || typeof input !== 'string') {
|
||||
throw new Error('No input provided');
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
|
||||
// Check if it looks like a URL
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const error = url.searchParams.get('error');
|
||||
|
||||
if (error) {
|
||||
throw new Error(`OAuth error: ${error}`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No authorization code found in URL');
|
||||
}
|
||||
|
||||
return { code, state };
|
||||
} catch (e) {
|
||||
if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error('Invalid URL format');
|
||||
}
|
||||
}
|
||||
|
||||
// Assume it's a raw code
|
||||
// Google auth codes typically start with "4/" and are long
|
||||
if (trimmed.length < 10) {
|
||||
throw new Error('Input is too short to be a valid authorization code');
|
||||
}
|
||||
|
||||
return { code: trimmed, state: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a local server to receive the OAuth callback
|
||||
* Returns a promise that resolves with the authorization code
|
||||
@@ -82,10 +132,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
const error = url.searchParams.get('error');
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<head><meta charset="UTF-8"><title>Authentication Failed</title></head>
|
||||
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
||||
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
||||
<p>Error: ${error}</p>
|
||||
@@ -99,10 +149,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
}
|
||||
|
||||
if (state !== expectedState) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<head><meta charset="UTF-8"><title>Authentication Failed</title></head>
|
||||
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
||||
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
||||
<p>State mismatch - possible CSRF attack.</p>
|
||||
@@ -116,10 +166,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<head><meta charset="UTF-8"><title>Authentication Failed</title></head>
|
||||
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
||||
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
||||
<p>No authorization code received.</p>
|
||||
@@ -133,10 +183,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
}
|
||||
|
||||
// Success!
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>Authentication Successful</title></head>
|
||||
<head><meta charset="UTF-8"><title>Authentication Successful</title></head>
|
||||
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
||||
<h1 style="color: #28a745;">✅ Authentication Successful!</h1>
|
||||
<p>You can close this window and return to the terminal.</p>
|
||||
@@ -339,6 +389,7 @@ export async function completeOAuthFlow(code, verifier) {
|
||||
|
||||
export default {
|
||||
getAuthorizationUrl,
|
||||
extractCodeFromInput,
|
||||
startCallbackServer,
|
||||
exchangeCode,
|
||||
refreshAccessToken,
|
||||
|
||||
Reference in New Issue
Block a user