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:
@@ -6,10 +6,80 @@
|
||||
* - Windows compatibility (no CLI dependency)
|
||||
* - Native performance
|
||||
* - Synchronous API (simple error handling)
|
||||
*
|
||||
* Includes auto-rebuild capability for handling Node.js version updates
|
||||
* that cause native module incompatibility.
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { createRequire } from 'module';
|
||||
import { ANTIGRAVITY_DB_PATH } from '../constants.js';
|
||||
import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { NativeModuleError } from '../errors.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// Lazy-loaded Database constructor
|
||||
let Database = null;
|
||||
let moduleLoadError = null;
|
||||
|
||||
/**
|
||||
* Load the better-sqlite3 module with auto-rebuild on version mismatch
|
||||
* Uses synchronous require to maintain API compatibility
|
||||
* @returns {Function} The Database constructor
|
||||
* @throws {Error} If module cannot be loaded even after rebuild
|
||||
*/
|
||||
function loadDatabaseModule() {
|
||||
// Return cached module if already loaded
|
||||
if (Database) return Database;
|
||||
|
||||
// Re-throw cached error if previous load failed permanently
|
||||
if (moduleLoadError) throw moduleLoadError;
|
||||
|
||||
try {
|
||||
Database = require('better-sqlite3');
|
||||
return Database;
|
||||
} catch (error) {
|
||||
if (isModuleVersionError(error)) {
|
||||
logger.warn('[Database] Native module version mismatch detected');
|
||||
|
||||
if (attemptAutoRebuild(error)) {
|
||||
// Clear require cache and retry
|
||||
try {
|
||||
const resolvedPath = require.resolve('better-sqlite3');
|
||||
// Clear the module and all its dependencies from cache
|
||||
clearRequireCache(resolvedPath, require.cache);
|
||||
|
||||
Database = require('better-sqlite3');
|
||||
logger.success('[Database] Module reloaded successfully after rebuild');
|
||||
return Database;
|
||||
} catch (retryError) {
|
||||
// Rebuild succeeded but reload failed - user needs to restart
|
||||
moduleLoadError = new NativeModuleError(
|
||||
'Native module rebuild completed. Please restart the server to apply the fix.',
|
||||
true, // rebuildSucceeded
|
||||
true // restartRequired
|
||||
);
|
||||
logger.info('[Database] Rebuild succeeded - server restart required');
|
||||
throw moduleLoadError;
|
||||
}
|
||||
} else {
|
||||
moduleLoadError = new NativeModuleError(
|
||||
'Failed to auto-rebuild native module. Please run manually:\n' +
|
||||
' npm rebuild better-sqlite3\n' +
|
||||
'Or if using npx, find the package location in the error and run:\n' +
|
||||
' cd /path/to/better-sqlite3 && npm rebuild',
|
||||
false, // rebuildSucceeded
|
||||
false // restartRequired
|
||||
);
|
||||
throw moduleLoadError;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-version-mismatch error, just throw it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Antigravity database for authentication status
|
||||
@@ -18,10 +88,11 @@ import { ANTIGRAVITY_DB_PATH } from '../constants.js';
|
||||
* @throws {Error} If database doesn't exist, query fails, or no auth status found
|
||||
*/
|
||||
export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
|
||||
const Db = loadDatabaseModule();
|
||||
let db;
|
||||
try {
|
||||
// Open database in read-only mode
|
||||
db = new Database(dbPath, {
|
||||
db = new Db(dbPath, {
|
||||
readonly: true,
|
||||
fileMustExist: true
|
||||
});
|
||||
@@ -56,6 +127,10 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
|
||||
if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
|
||||
throw error;
|
||||
}
|
||||
// Re-throw native module errors from loadDatabaseModule without wrapping
|
||||
if (error instanceof NativeModuleError) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to read Antigravity database: ${error.message}`);
|
||||
} finally {
|
||||
// Always close database connection
|
||||
@@ -73,7 +148,8 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
|
||||
export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) {
|
||||
let db;
|
||||
try {
|
||||
db = new Database(dbPath, {
|
||||
const Db = loadDatabaseModule();
|
||||
db = new Db(dbPath, {
|
||||
readonly: true,
|
||||
fileMustExist: true
|
||||
});
|
||||
|
||||
@@ -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