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:
Wha1eChai
2026-01-09 18:08:45 +08:00
22 changed files with 1100 additions and 96 deletions

View File

@@ -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,