3535const CLI_INSTALL_LOCK_FILE_NAME = '.install.lock'
3636const DOWNLOAD_MESSAGE_DELAY_MS = 2_000
3737const HTTPS_TIMEOUT_MS = 30_000
38- const IPC_HANDSHAKE_TIMEOUT_MS = 5_000
3938// 30 seconds total.
4039const LOCK_MAX_RETRIES = 60
4140const LOCK_RETRY_DELAY_MS = 500
@@ -45,9 +44,12 @@ const NPM_REGISTRY =
4544 'https://registry.npmjs.org'
4645const SOCKET_CLI_DIR =
4746 process . env [ 'SOCKET_CLI_DIR' ] || path . join ( SOCKET_HOME , '_cli' )
47+ const SOCKET_CLI_PACKAGE_DIR = path . join ( SOCKET_CLI_DIR , 'package' )
4848const SOCKET_CLI_PACKAGE =
4949 process . env [ 'SOCKET_CLI_PACKAGE' ] || '@socketsecurity/cli'
50- const SOCKET_CLI_PACKAGE_JSON = path . join ( SOCKET_CLI_DIR , 'package.json' )
50+ const SOCKET_CLI_PACKAGE_JSON = path . join ( SOCKET_CLI_PACKAGE_DIR , 'package.json' )
51+ // Minimum Node.js version for system Node (v22 = Active LTS)
52+ const MIN_NODE_VERSION = parseInt ( process . env [ 'MIN_NODE_VERSION' ] || '22' , 10 )
5153
5254// ============================================================================
5355// Helper utilities
@@ -70,6 +72,7 @@ function formatError(error: unknown): string {
7072 return error instanceof Error ? error . message : String ( error )
7173}
7274
75+
7376/**
7477 * Sanitize tarball path to prevent directory traversal attacks.
7578 * Ensures extracted files stay within the target directory.
@@ -238,8 +241,8 @@ async function downloadAndInstallPackage(version: string): Promise<void> {
238241 let tarballPath : string | undefined
239242
240243 try {
241- // Ensure CLI directory exists before acquiring lock.
242- await fs . mkdir ( SOCKET_CLI_DIR , { recursive : true } )
244+ // Ensure CLI package directory exists before acquiring lock.
245+ await fs . mkdir ( SOCKET_CLI_PACKAGE_DIR , { recursive : true } )
243246
244247 // Acquire installation lock.
245248 lockPath = await acquireLock ( )
@@ -347,7 +350,7 @@ async function extractTarball(tarballPath: string): Promise<void> {
347350 // Sanitize file path to prevent directory traversal attacks.
348351 // This removes 'package/' prefix, strips '..' segments, and normalizes separators.
349352 const sanitizedPath = sanitizeTarballPath ( file . name )
350- const targetPath = path . join ( SOCKET_CLI_DIR , sanitizedPath )
353+ const targetPath = path . join ( SOCKET_CLI_PACKAGE_DIR , sanitizedPath )
351354
352355 if ( file . type === 'directory' ) {
353356 await retryWithBackoff ( ( ) =>
@@ -409,6 +412,23 @@ async function extractTarball(tarballPath: string): Promise<void> {
409412 )
410413 }
411414 }
415+
416+ // CRITICAL: Ensure bin/ and shadow-bin/ files are executable.
417+ // These directories contain entry points that must be executable.
418+ const relativePath = path . relative ( SOCKET_CLI_PACKAGE_DIR , targetPath )
419+ if (
420+ relativePath . startsWith ( 'bin' + path . sep ) ||
421+ relativePath . startsWith ( 'shadow-bin' + path . sep ) ||
422+ relativePath . startsWith ( 'dist' + path . sep + 'shadow' )
423+ ) {
424+ await retryWithBackoff ( ( ) => fs . chmod ( targetPath , 0o755 ) ) . catch (
425+ error => {
426+ console . error (
427+ `Warning: Failed to make ${ targetPath } executable: ${ formatError ( error ) } ` ,
428+ )
429+ } ,
430+ )
431+ }
412432 }
413433 }
414434
@@ -626,9 +646,16 @@ async function spawnEmbeddedNode(
626646 cliPath : string ,
627647 args : string [ ] | readonly string [ ] ,
628648) : Promise < void > {
629- return await spawnNodeProcess ( process . execPath , args , {
649+ // Build command arguments: security/warning flags + cli path + user args
650+ const commandArgs = [
651+ '--no-addons' ,
652+ '--no-warnings' ,
653+ cliPath ,
654+ ...args ,
655+ ]
656+
657+ return await spawnNodeProcess ( process . execPath , commandArgs , {
630658 env : process . env ,
631- cliPathForEmbedded : cliPath ,
632659 } )
633660}
634661
@@ -641,10 +668,9 @@ async function spawnNodeProcess(
641668 commandArgs : string [ ] | readonly string [ ] ,
642669 options : {
643670 env : NodeJS . ProcessEnv
644- cliPathForEmbedded ?: string
645671 } ,
646672) : Promise < void > {
647- const { cliPathForEmbedded , env } = options
673+ const { env } = options
648674
649675 const child = spawn ( command , commandArgs , {
650676 stdio : [ 'inherit' , 'inherit' , 'inherit' , 'ipc' ] ,
@@ -657,7 +683,6 @@ async function spawnNodeProcess(
657683 const handshake : {
658684 SOCKET_IPC_HANDSHAKE : {
659685 SOCKET_CLI_STUB_PATH ?: string
660- SOCKET_CLI_PATH ?: string
661686 }
662687 } = {
663688 SOCKET_IPC_HANDSHAKE : { } ,
@@ -666,9 +691,6 @@ async function spawnNodeProcess(
666691 if ( stubPath ) {
667692 handshake . SOCKET_IPC_HANDSHAKE . SOCKET_CLI_STUB_PATH = stubPath
668693 }
669- if ( cliPathForEmbedded ) {
670- handshake . SOCKET_IPC_HANDSHAKE . SOCKET_CLI_PATH = cliPathForEmbedded
671- }
672694
673695 if ( Object . keys ( handshake . SOCKET_IPC_HANDSHAKE ) . length > 0 ) {
674696 try {
@@ -710,7 +732,15 @@ async function spawnSystemNode(
710732 cliPath : string ,
711733 args : string [ ] | readonly string [ ] ,
712734) : Promise < void > {
713- return await spawnNodeProcess ( 'node' , [ cliPath , ...args ] , {
735+ // Build command arguments: security/warning flags + cli path + user args
736+ const commandArgs = [
737+ '--no-addons' ,
738+ '--no-warnings' ,
739+ cliPath ,
740+ ...args ,
741+ ]
742+
743+ return await spawnNodeProcess ( 'node' , commandArgs , {
714744 env : process . env ,
715745 } )
716746}
@@ -724,55 +754,7 @@ async function spawnSystemNode(
724754 * Ensures CLI is installed, then spawns it with system Node.js.
725755 */
726756async function main ( ) : Promise < void > {
727- // Check if we're being spawned to execute the CLI directly (bypass bootstrap).
728- // Parent sends CLI path via IPC handshake.
729- if ( process . send ) {
730- const cliPath = await new Promise < string | undefined > ( resolve => {
731- const timeout = setTimeout (
732- ( ) => resolve ( undefined ) ,
733- IPC_HANDSHAKE_TIMEOUT_MS ,
734- )
735- process . on ( 'message' , msg => {
736- if (
737- msg !== null &&
738- typeof msg === 'object' &&
739- 'SOCKET_IPC_HANDSHAKE' in msg &&
740- msg . SOCKET_IPC_HANDSHAKE !== null &&
741- typeof msg . SOCKET_IPC_HANDSHAKE === 'object' &&
742- msg . SOCKET_IPC_HANDSHAKE &&
743- 'SOCKET_CLI_PATH' in msg . SOCKET_IPC_HANDSHAKE
744- ) {
745- clearTimeout ( timeout )
746- resolve ( msg . SOCKET_IPC_HANDSHAKE . SOCKET_CLI_PATH as string )
747- }
748- } )
749- } )
750-
751- if ( cliPath ) {
752- // Verify CLI file exists before attempting to require it.
753- if ( ! existsSync ( cliPath ) ) {
754- console . error (
755- `Fatal: CLI entry point not found at ${ cliPath } . Installation may be corrupted.` ,
756- )
757- // eslint-disable-next-line n/no-process-exit
758- process . exit ( 1 )
759- }
760-
761- // Set process.argv to include CLI path and user arguments.
762- process . argv = [ process . argv [ 0 ] ! , cliPath , ...process . argv . slice ( 1 ) ]
763- // Load and execute the CLI with embedded Node.js.
764- try {
765- require ( cliPath )
766- } catch ( error ) {
767- console . error (
768- `Fatal: Failed to load CLI from ${ cliPath } : ${ formatError ( error ) } ` ,
769- )
770- // eslint-disable-next-line n/no-process-exit
771- process . exit ( 1 )
772- }
773- return
774- }
775- }
757+ // No IPC bypass check needed - we pass CLI path as command line argument now
776758
777759 try {
778760 // Ensure Socket home directory exists with better error messages.
@@ -816,7 +798,7 @@ async function main(): Promise<void> {
816798 const args = process . argv . slice ( 2 )
817799
818800 // process.env.MIN_NODE_VERSION is inlined at build time.
819- const minNodeVersion = parseInt ( process . env [ 'MIN_NODE_VERSION' ] ?? '0' , 0 )
801+ const minNodeVersion = parseInt ( process . env [ 'MIN_NODE_VERSION' ] ?? '0' , 10 ) || MIN_NODE_VERSION
820802
821803 const systemNodeVersion = await getSystemNodeVersion ( minNodeVersion )
822804
0 commit comments