@@ -63,12 +63,22 @@ const BINARY_SEARCH_PATHS = [
6363]
6464
6565// Binary name patterns to search for
66+ // Windows binaries can be .exe or .bat files
6667const BINARY_PATTERNS = [
6768 'curl-impersonate' ,
6869 'curl-impersonate.exe' ,
70+ 'curl-impersonate.bat' ,
71+ 'curl_chrome*.exe' ,
72+ 'curl_chrome*.bat' ,
6973 'curl_chrome*' ,
74+ 'curl_firefox*.exe' ,
75+ 'curl_firefox*.bat' ,
7076 'curl_firefox*' ,
77+ 'curl_edge*.exe' ,
78+ 'curl_edge*.bat' ,
7179 'curl_edge*' ,
80+ 'curl_safari*.exe' ,
81+ 'curl_safari*.bat' ,
7282 'curl_safari*' ,
7383]
7484
@@ -135,8 +145,16 @@ const findExistingBinary = (browser: string = ''): string | null => {
135145 const packageDir = getPackageDir ( )
136146 const packageBinariesDir = path . resolve ( packageDir , 'cuimp/binaries' )
137147
138- // Create search paths including both directories
139- const searchPaths = [ homeBinariesDir , packageBinariesDir , ...BINARY_SEARCH_PATHS ]
148+ // On Windows, binaries are extracted to a 'bin' subdirectory
149+ // Create search paths including both directories and Windows-specific bin subdirectory
150+ const isWindows = process . platform === 'win32'
151+ const searchPaths = [
152+ homeBinariesDir ,
153+ ...( isWindows ? [ path . resolve ( homeBinariesDir , 'bin' ) ] : [ ] ) ,
154+ packageBinariesDir ,
155+ ...( isWindows ? [ path . resolve ( packageBinariesDir , 'bin' ) ] : [ ] ) ,
156+ ...BINARY_SEARCH_PATHS
157+ ]
140158
141159 for ( const searchPath of searchPaths ) {
142160 for ( const pattern of patternsToSearch ) {
@@ -211,6 +229,10 @@ const downloadAndExtractBinary = async (
211229 // macos uses specific naming: x86_64-macos, arm64-macos, etc.
212230 const macosArch = architecture === 'x64' ? 'x86_64' : 'arm64'
213231 assetName = `curl-impersonate-${ latestVersion } .${ macosArch } -macos.tar.gz`
232+ } else if ( platform === 'windows' ) {
233+ // Windows uses libcurl-impersonate prefix and win32 suffix: x86_64-win32, arm64-win32, etc.
234+ const windowsArch = architecture === 'x64' ? 'x86_64' : 'arm64'
235+ assetName = `libcurl-impersonate-${ latestVersion } .${ windowsArch } -win32.tar.gz`
214236 } else {
215237 // Other platforms use the original naming
216238 assetName = `curl-impersonate-${ latestVersion } .${ architecture } -${ platform } .tar.gz`
@@ -251,45 +273,95 @@ const downloadAndExtractBinary = async (
251273 // Clean up temporary file
252274 fs . unlinkSync ( tempFileName )
253275
254- // Find the extracted binary file in the binaries directory
255- // The main binary is usually named 'curl-impersonate'
256- const mainBinaryName = 'curl-impersonate'
257- const binaryPath = path . resolve ( binariesDir , mainBinaryName )
276+ // On Windows, binaries are extracted to a 'bin' subdirectory
277+ // On other platforms, they're extracted directly to binariesDir
278+ const searchDirs = platform === 'windows'
279+ ? [ path . resolve ( binariesDir , 'bin' ) , binariesDir ]
280+ : [ binariesDir ]
258281
259- // Check if the main binary was extracted
260- if ( ! fs . existsSync ( binaryPath ) ) {
261- // If main binary not found, look for browser-specific binaries
262- const browserSpecificPattern = `curl_${ browser } *`
263- const files = fs . readdirSync ( binariesDir )
264- const matchingFiles = files . filter ( file => {
265- const regex = new RegExp ( browserSpecificPattern . replace ( '*' , '.*' ) )
266- return regex . test ( file )
267- } )
282+ // Binary name patterns to search for (Windows uses .exe or .bat extension)
283+ const binaryExtensions = platform === 'windows' ? [ '.exe' , '.bat' , '' ] : [ '' ]
284+ const mainBinaryNames = binaryExtensions . map ( ext => `curl-impersonate${ ext } ` )
285+ const browserSpecificPattern = `curl_${ browser } *`
286+
287+ let binaryPath : string | null = null
288+
289+ // First, try to find the main binary (curl-impersonate)
290+ for ( const searchDir of searchDirs ) {
291+ if ( ! fs . existsSync ( searchDir ) ) continue
268292
269- if ( matchingFiles . length > 0 ) {
270- // Use the highest version browser-specific binary
271- const sortedFiles = matchingFiles . sort ( ( a , b ) => {
272- const versionA = extractVersionNumber ( a )
273- const versionB = extractVersionNumber ( b )
274- return versionB - versionA // Sort in descending order (highest first)
275- } )
276- const bestMatch = sortedFiles [ 0 ]
277- const browserBinaryPath = path . resolve ( binariesDir , bestMatch )
293+ for ( const mainBinaryName of mainBinaryNames ) {
294+ const candidatePath = path . resolve ( searchDir , mainBinaryName )
295+ if ( fs . existsSync ( candidatePath ) && fs . statSync ( candidatePath ) . isFile ( ) ) {
296+ binaryPath = candidatePath
297+ break
298+ }
299+ }
300+ if ( binaryPath ) break
301+ }
302+
303+ // If main binary not found, look for browser-specific binaries
304+ if ( ! binaryPath ) {
305+ for ( const searchDir of searchDirs ) {
306+ if ( ! fs . existsSync ( searchDir ) ) continue
278307
279- // Set executable permissions on the browser-specific binary
280- fs . chmodSync ( browserBinaryPath , 0o755 )
308+ const files = fs . readdirSync ( searchDir )
309+ const matchingFiles = files . filter ( file => {
310+ const regex = new RegExp ( browserSpecificPattern . replace ( '*' , '.*' ) )
311+ return regex . test ( file ) && fs . statSync ( path . resolve ( searchDir , file ) ) . isFile ( )
312+ } )
281313
282- return {
283- binaryPath : browserBinaryPath ,
284- version : actualVersion
314+ if ( matchingFiles . length > 0 ) {
315+ // Use the highest version browser-specific binary
316+ const sortedFiles = matchingFiles . sort ( ( a , b ) => {
317+ const versionA = extractVersionNumber ( a )
318+ const versionB = extractVersionNumber ( b )
319+ return versionB - versionA // Sort in descending order (highest first)
320+ } )
321+ const bestMatch = sortedFiles [ 0 ]
322+ binaryPath = path . resolve ( searchDir , bestMatch )
323+ break
285324 }
286325 }
287-
288- throw new Error ( `Binary not found after extraction. Expected: ${ binaryPath } ` )
289326 }
290327
291- // Set executable permissions on the main binary
292- fs . chmodSync ( binaryPath , 0o755 )
328+ if ( ! binaryPath ) {
329+ throw new Error (
330+ `Binary not found after extraction. Searched in: ${ searchDirs . join ( ', ' ) } . ` +
331+ `Expected: curl-impersonate${ platform === 'windows' ? '.exe' : '' } or curl_${ browser } *`
332+ )
333+ }
334+
335+ // Set executable permissions on the binary (chmod may not work on Windows, but it's safe to try)
336+ try {
337+ fs . chmodSync ( binaryPath , 0o755 )
338+ } catch ( error ) {
339+ // On Windows, chmod might fail, but that's okay - the file is still executable
340+ if ( platform !== 'windows' ) {
341+ throw error
342+ }
343+ }
344+
345+ // On Windows, download CA bundle if not present (required for SSL verification)
346+ if ( platform === 'windows' ) {
347+ const binDir = path . dirname ( binaryPath )
348+ const caBundlePath = path . join ( binDir , 'curl-ca-bundle.crt' )
349+ if ( ! fs . existsSync ( caBundlePath ) ) {
350+ logger . info ( 'Downloading CA certificate bundle for Windows...' )
351+ try {
352+ const caResponse = await fetch ( 'https://curl.se/ca/cacert.pem' )
353+ if ( caResponse . ok ) {
354+ const caBundle = await caResponse . text ( )
355+ fs . writeFileSync ( caBundlePath , caBundle )
356+ logger . info ( `CA bundle saved to ${ caBundlePath } ` )
357+ } else {
358+ logger . warn ( 'Failed to download CA bundle - SSL verification may fail' )
359+ }
360+ } catch ( caError ) {
361+ logger . warn ( `Failed to download CA bundle: ${ caError instanceof Error ? caError . message : String ( caError ) } ` )
362+ }
363+ }
364+ }
293365
294366 return {
295367 binaryPath : binaryPath ,
@@ -356,21 +428,18 @@ export const parseDescriptor = async (descriptor: CuimpDescriptor, logger: Logge
356428 if ( ! forceDownload ) {
357429 const existingBinary = findExistingBinary ( browser )
358430 if ( existingBinary ) {
359- const existingVersion = extractVersionNumber ( path . basename ( existingBinary ) ) . toString ( )
360-
361- // For 'latest', accept any existing binary
362- // For specific version, check if it matches
363- const versionMatches = version === 'latest' || existingVersion === version
431+ // Extract browser version from filename (e.g., curl_chrome136 -> 136)
432+ // Note: This is the browser version, not the curl-impersonate release version
433+ const browserVersion = extractVersionNumber ( path . basename ( existingBinary ) ) . toString ( )
364434
365- if ( versionMatches ) {
366- logger . info ( `Found existing binary: ${ existingBinary } ` )
367- return {
368- binaryPath : existingBinary ,
369- isDownloaded : false ,
370- version : existingVersion || 'unknown'
371- }
372- } else {
373- logger . warn ( `Found binary version ${ existingVersion } , but version ${ version } was requested. Re-downloading...` )
435+ // Accept any existing binary for the requested browser
436+ // The 'version' field in descriptor refers to curl-impersonate release version,
437+ // which we can't easily determine from the cached binary filename
438+ logger . info ( `Found existing binary: ${ existingBinary } ` )
439+ return {
440+ binaryPath : existingBinary ,
441+ isDownloaded : false ,
442+ version : browserVersion || 'unknown'
374443 }
375444 }
376445 } else {
0 commit comments