@ -7,13 +7,21 @@
import { contextBridge , ipcRenderer } from 'electron' ;
// Simple logger for preload script
// Enhanced logger for preload script
const logger = {
log : ( . . . args : unknown [ ] ) = > console . log ( '[Preload]' , . . . args ) ,
error : ( . . . args : unknown [ ] ) = > console . error ( '[Preload]' , . . . args ) ,
info : ( . . . args : unknown [ ] ) = > console . info ( '[Preload]' , . . . args ) ,
warn : ( . . . args : unknown [ ] ) = > console . warn ( '[Preload]' , . . . args ) ,
debug : ( . . . args : unknown [ ] ) = > console . debug ( '[Preload]' , . . . args ) ,
sqlite : {
log : ( operation : string , . . . args : unknown [ ] ) = >
console . log ( '[Preload][SQLite]' , operation , . . . args ) ,
error : ( operation : string , error : unknown ) = >
console . error ( '[Preload][SQLite]' , operation , 'failed:' , error ) ,
debug : ( operation : string , . . . args : unknown [ ] ) = >
console . debug ( '[Preload][SQLite]' , operation , . . . args )
}
} ;
// Types for SQLite connection options
@ -30,38 +38,72 @@ interface SQLiteConnectionOptions {
// Define valid channels for security
const VALID_CHANNELS = {
send : [ 'toMain' , 'sqlite-status' ] ,
receive : [ 'fromMain' , 'sqlite-ready' , 'database-status' ] ,
invoke : [ 'sqlite-is-available' , 'sqlite-echo' , 'sqlite-create-connection' , 'sqlite-execute' , 'sqlite-query' , 'sqlite-run' , 'sqlite-close-connection' , 'get-path' , 'get-base-path' ]
send : [ 'toMain' , 'sqlite-status' ] as const ,
receive : [ 'fromMain' , 'sqlite-ready' , 'database-status' ] as const ,
invoke : [
'sqlite-is-available' ,
'sqlite-echo' ,
'sqlite-create-connection' ,
'sqlite-execute' ,
'sqlite-query' ,
'sqlite-run' ,
'sqlite-close-connection' ,
'get-path' ,
'get-base-path'
] as const
} ;
type ValidSendChannel = typeof VALID_CHANNELS . send [ number ] ;
type ValidReceiveChannel = typeof VALID_CHANNELS . receive [ number ] ;
type ValidInvokeChannel = typeof VALID_CHANNELS . invoke [ number ] ;
// Create a secure IPC bridge
const createSecureIPCBridge = ( ) = > {
return {
send : ( channel : string , data : unknown ) = > {
if ( VALID_CHANNELS . send . includes ( channel ) ) {
if ( VALID_CHANNELS . send . includes ( channel as ValidSendChannel ) ) {
logger . debug ( 'IPC Send:' , channel , data ) ;
ipcRenderer . send ( channel , data ) ;
} else {
logger . warn ( ` [Preload] Attempted to send on invalid channel: ${ channel } ` ) ;
}
} ,
receive : ( channel : string , func : ( . . . args : unknown [ ] ) = > void ) = > {
if ( VALID_CHANNELS . receive . includes ( channel ) ) {
ipcRenderer . on ( channel , ( _event , . . . args ) = > func ( . . . args ) ) ;
if ( VALID_CHANNELS . receive . includes ( channel as ValidReceiveChannel ) ) {
logger . debug ( 'IPC Receive:' , channel ) ;
ipcRenderer . on ( channel , ( _event , . . . args ) = > {
logger . debug ( 'IPC Received:' , channel , args ) ;
func ( . . . args ) ;
} ) ;
} else {
logger . warn ( ` [Preload] Attempted to receive on invalid channel: ${ channel } ` ) ;
}
} ,
once : ( channel : string , func : ( . . . args : unknown [ ] ) = > void ) = > {
if ( VALID_CHANNELS . receive . includes ( channel ) ) {
ipcRenderer . once ( channel , ( _event , . . . args ) = > func ( . . . args ) ) ;
if ( VALID_CHANNELS . receive . includes ( channel as ValidReceiveChannel ) ) {
logger . debug ( 'IPC Once:' , channel ) ;
ipcRenderer . once ( channel , ( _event , . . . args ) = > {
logger . debug ( 'IPC Received Once:' , channel , args ) ;
func ( . . . args ) ;
} ) ;
} else {
logger . warn ( ` [Preload] Attempted to receive once on invalid channel: ${ channel } ` ) ;
}
} ,
invoke : async ( channel : string , . . . args : unknown [ ] ) = > {
if ( VALID_CHANNELS . invoke . includes ( channel ) ) {
return await ipcRenderer . invoke ( channel , . . . args ) ;
if ( VALID_CHANNELS . invoke . includes ( channel as ValidInvokeChannel ) ) {
logger . debug ( 'IPC Invoke:' , channel , args ) ;
try {
const result = await ipcRenderer . invoke ( channel , . . . args ) ;
logger . debug ( 'IPC Invoke Result:' , channel , result ) ;
return result ;
} catch ( error ) {
logger . error ( 'IPC Invoke Error:' , channel , error ) ;
throw error ;
}
} else {
logger . warn ( ` [Preload] Attempted to invoke on invalid channel: ${ channel } ` ) ;
throw new Error ( ` Invalid channel: ${ channel } ` ) ;
@ -75,57 +117,60 @@ const createSQLiteProxy = () => {
const MAX_RETRIES = 3 ;
const RETRY_DELAY = 1000 ;
const withRetry = async ( operation : ( . . . args : unknown [ ] ) = > Promise < unknown > , . . . args : unknown [ ] ) = > {
let lastError ;
const withRetry = async < T > ( operation : string , . . . args : unknown [ ] ) : Promise < T > = > {
let lastError : Error | undefined ;
for ( let attempt = 1 ; attempt <= MAX_RETRIES ; attempt ++ ) {
try {
return await operation ( . . . args ) ;
logger . sqlite . debug ( operation , 'attempt' , attempt , args ) ;
const result = await ipcRenderer . invoke ( ` sqlite- ${ operation } ` , . . . args ) ;
logger . sqlite . log ( operation , 'success' , result ) ;
return result as T ;
} catch ( error ) {
lastError = error ;
lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
logger . sqlite . error ( operation , error ) ;
if ( attempt < MAX_RETRIES ) {
logger . warn ( ` [Preload] SQLite operation failed (attempt ${ attempt } / ${ MAX_RETRIES } ), retrying... ` , error ) ;
logger . warn ( ` [Preload] SQLite ${ operation } failed (attempt ${ attempt } / ${ MAX_RETRIES } ), retrying... ` , error ) ;
await new Promise ( resolve = > setTimeout ( resolve , RETRY_DELAY ) ) ;
}
}
}
throw new Error ( ` SQLite operation failed after ${ MAX_RETRIES } attempts: ${ lastError ? . message || "Unknown error" } ` ) ;
} ;
const wrapOperation = ( method : string ) = > {
return async ( . . . args : unknown [ ] ) = > {
try {
const handlerName = ` sqlite- ${ method } ` ;
return await withRetry ( ipcRenderer . invoke , handlerName , . . . args ) ;
} catch ( error ) {
logger . error ( ` [Preload] SQLite ${ method } failed: ` , error ) ;
throw error ;
}
} ;
throw new Error ( ` SQLite ${ operation } failed after ${ MAX_RETRIES } attempts: ${ lastError ? . message || "Unknown error" } ` ) ;
} ;
return {
echo : wrapOperation ( 'echo ' ) ,
createConnection : wrapOperation ( 'create-connection' ) ,
closeConnection : wrapOperation ( 'close-connection' ) ,
execute : wrapOperation ( 'execute' ) ,
query : wrapOperation ( 'query' ) ,
run : wrapOperation ( 'run' ) ,
isAvailable : wrapOperation ( 'is-available' ) ,
isAvailable : ( ) = > withRetry ( 'is-available' ) ,
echo : ( value : string ) = > withRetry ( 'echo' , { value } ) ,
createConnection : ( options : SQLiteConnectionOptions ) = > withRetry ( 'create-connection' , options ) ,
closeConnection : ( options : { database : string } ) = > withRetry ( 'close-connection' , options ) ,
query : ( options : { statement : string ; values? : unknown [ ] } ) = > withRetry ( 'query' , options ) ,
run : ( options : { statement : string ; values? : unknown [ ] } ) = > withRetry ( 'run' , options ) ,
execute : ( options : { statements : { statement : string ; values? : unknown [ ] } [ ] } ) = > withRetry ( 'execute' , options ) ,
getPlatform : ( ) = > Promise . resolve ( 'electron' )
} ;
} ;
try {
// Expose the secure IPC bridge and SQLite proxy
contextBridge . exposeInMainWorld ( 'electron' , {
const electronAPI = {
ipcRenderer : createSecureIPCBridge ( ) ,
sqlite : createSQLiteProxy ( ) ,
env : {
platform : 'electron' ,
isDev : process.env.NODE_ENV === 'development'
}
} ;
// Log the exposed API for debugging
logger . debug ( 'Exposing Electron API:' , {
hasIpcRenderer : ! ! electronAPI . ipcRenderer ,
hasSqlite : ! ! electronAPI . sqlite ,
sqliteMethods : Object.keys ( electronAPI . sqlite ) ,
env : electronAPI.env
} ) ;
contextBridge . exposeInMainWorld ( 'electron' , electronAPI ) ;
logger . info ( '[Preload] IPC bridge and SQLite proxy initialized successfully' ) ;
} catch ( error ) {
logger . error ( '[Preload] Failed to initialize IPC bridge:' , error ) ;