ProcessProxy
A modern npm package (requiring Node 22+) written in TypeScript that enables developers to interact with a native executable, reading its stdin, writing to its stdout, and stderr streams, reading its exit code, arguments, current directory, and environment variables.
⚠️ Development Status
This project is in early development and relies heavily on automated code generation using GitHub Copilot CLI. While the implementation follows a clear design specification, it has not been extensively tested in production environments. Users should expect:
- Potential unexpected behaviors or edge cases
- Limited cross-platform testing (primarily tested on macOS arm64)
- Possible API changes as the project matures
- Incomplete error handling for all scenarios
Use at your own risk. Contributions, bug reports, and testing on different platforms are highly encouraged.
Installation
npm install process-proxy
Building
This package includes a native executable that must be built before use:
npm run build
This will compile both the TypeScript library and the native C executable.
Usage
Basic Example
import { createProxyProcessServer, getProxyCommandPath } from 'process-proxy'
import { spawn } from 'child_process'
import { AddressInfo } from 'net'
const server = createProxyProcessServer((connection) => {
console.log('Native process connected!')
connection.getArgs().then((args) => console.log('Arguments:', args))
connection.getEnv().then((env) => console.log('Environment:', env))
connection.getCwd().then((cwd) => console.log('Working Directory:', cwd))
connection.stdout.write('Hello from ProcessProxy!\n')
connection.stdin.on('data', (data) => {
console.log('Received from stdin:', data.toString())
})
connection.on('close', () => {
console.log('Process disconnected')
})
})
const port = await new Promise<number>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve((server.address() as AddressInfo).port)
})
})
const nativeExe = getProxyCommandPath()
const child = spawn(nativeExe, ['arg1', 'arg2'], {
env: {
...process.env,
PROCESS_PROXY_PORT: port.toString(),
},
})
Proxying stdin/stdout/stderr
server.on('connection', (connection) => {
process.stdin.pipe(connection.stdout)
connection.stdin.pipe(process.stdout)
connection.stderr.on('data', (data) => {
process.stderr.write(data)
})
})
Controlling the Process
server.on('connection', async (connection) => {
await connection.exit(0)
})
Closing Streams
server.on('connection', async (connection) => {
connection.stdin.end()
})
API
createProxyProcessServer()
Creates a TCP server that listens for incoming connections from native processes. Returns a standard Node.js net.Server instance.
import { createProxyProcessServer } from 'process-proxy'
import { AddressInfo } from 'net'
const server = createProxyProcessServer((connection) => {
console.log('New connection!')
connection.getArgs().then(console.log)
})
const port = await new Promise<number>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve((server.address() as AddressInfo).port)
})
})
server.on('listening', () => console.log('Server started'))
server.on('error', (err) => console.error('Server error:', err))
server.close(() => console.log('Server closed'))
Parameters:
listener: (connection: ProcessProxyConnection) => void - Callback invoked for each incoming connection
options?: ProxyProcessServerOptions - Optional configuration object:
validateConnection?: (token: string) => Promise<boolean> - Optional callback to validate the connection token during handshake. Receives the token from the handshake and should return a Promise resolving to true to accept the connection or false to reject it.
- All standard Node.js
net.ServerOpts options are also supported
Returns: Server - A standard Node.js net.Server instance
getProxyCommandPath()
Returns the absolute path to the native proxy executable.
import { getProxyCommandPath } from 'process-proxy'
const executablePath = getProxyCommandPath()
This utility function automatically:
- Resolves the correct path relative to the installed package
- Adds the
.exe suffix on Windows
- Works regardless of where the package is installed
ProcessProxyConnection
Represents a connection to a single instance of the native executable.
Properties
stdin: Readable - Readable stream for the executable's stdin
stdout: Writable - Writable stream for the executable's stdout
stderr: Writable - Writable stream for the executable's stderr
Methods
sendCommand(command: number, payload?: Buffer): Promise<Buffer> - Sends a raw command to the executable and returns the response
getArgs(): Promise<string[]> - Retrieves the command line arguments of the executable
getEnv(): Promise<{ [key: string]: string }> - Retrieves the environment variables of the executable
getCwd(): Promise<string> - Retrieves the current working directory of the executable
exit(code: number): Promise<void> - Exits the executable with the specified exit code
Events
close - Emitted when the connection is closed
error - Emitted when an error occurs. Listener signature: (error: Error) => void
Native Executable
The native executable is written in C and compiled using node-gyp. It connects to the TCP server specified by the PROCESS_PROXY_PORT environment variable.
Building the Native Executable
The native executable is built automatically when running npm run build or npm run build:native.
Usage
The native executable must be launched with the PROCESS_PROXY_PORT environment variable set:
PROCESS_PROXY_PORT=12345 ./build/Release/process-proxy [args...]
Security
⚠️ IMPORTANT SECURITY NOTE ⚠️
The TCP server only listens on localhost (127.0.0.1), but this does not provide complete security. Other processes and users with access to the network stack on the host machine can potentially connect to the TCP server.
Built-in Token Authentication
ProcessProxy includes a built-in authentication mechanism to validate connections during the handshake phase:
-
When the native executable connects, it sends a 146-byte handshake containing:
- Protocol header: "ProcessProxy 0002 " (18 bytes)
- Token: 128 bytes read from the
PROCESS_PROXY_TOKEN environment variable
-
The server validates this handshake and can optionally verify the token using a validateConnection callback
-
If authentication fails, the connection is immediately closed before any commands are processed
Using Token Authentication:
import crypto from 'crypto'
import { createProxyProcessServer, getProxyCommandPath } from 'process-proxy'
import { spawn } from 'child_process'
import { AddressInfo } from 'net'
const expectedToken = crypto.randomBytes(32).toString('hex')
const server = createProxyProcessServer(
(connection) => {
console.log('Authenticated connection established')
},
{
validateConnection: async (token) => {
return token === expectedToken
},
},
)
const port = await new Promise<number>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve((server.address() as AddressInfo).port)
})
})
const child = spawn(getProxyCommandPath(), ['arg1', 'arg2'], {
env: {
...process.env,
PROCESS_PROXY_PORT: port.toString(),
PROCESS_PROXY_TOKEN: expectedToken,
},
})
How it works:
- The native executable reads
PROCESS_PROXY_TOKEN from its environment and includes it in the handshake
- The server calls your
validateConnection callback with the received token
- If the callback returns
false or rejects, the connection is immediately closed
- Authentication happens before any commands are processed, preventing unauthorized access
Additional Security Recommendations:
- Always use
validateConnection in production environments
- Generate unique tokens per session using cryptographically secure random generators
- Use environment variables to pass tokens (never hardcode them)
- Implement additional authorization based on your application's security requirements
- Monitor for suspicious connection patterns and implement rate limiting if needed
Platform Support
License
This project is licensed under the MIT License - see the LICENSE file for details.