@voratiq/sandbox-runtime 0.7.0-voratiq1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +11 -0
  4. package/README.md +17 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +243 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +7 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/sandbox/generate-seccomp-filter.d.ts +56 -0
  14. package/dist/sandbox/generate-seccomp-filter.d.ts.map +1 -0
  15. package/dist/sandbox/generate-seccomp-filter.js +158 -0
  16. package/dist/sandbox/generate-seccomp-filter.js.map +1 -0
  17. package/dist/sandbox/http-proxy.d.ts +12 -0
  18. package/dist/sandbox/http-proxy.d.ts.map +1 -0
  19. package/dist/sandbox/http-proxy.js +489 -0
  20. package/dist/sandbox/http-proxy.js.map +1 -0
  21. package/dist/sandbox/linux-sandbox-utils.d.ts +111 -0
  22. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -0
  23. package/dist/sandbox/linux-sandbox-utils.js +518 -0
  24. package/dist/sandbox/linux-sandbox-utils.js.map +1 -0
  25. package/dist/sandbox/macos-sandbox-utils.d.ts +54 -0
  26. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -0
  27. package/dist/sandbox/macos-sandbox-utils.js +559 -0
  28. package/dist/sandbox/macos-sandbox-utils.js.map +1 -0
  29. package/dist/sandbox/sandbox-config.d.ts +170 -0
  30. package/dist/sandbox/sandbox-config.d.ts.map +1 -0
  31. package/dist/sandbox/sandbox-config.js +126 -0
  32. package/dist/sandbox/sandbox-config.js.map +1 -0
  33. package/dist/sandbox/sandbox-manager.d.ts +35 -0
  34. package/dist/sandbox/sandbox-manager.d.ts.map +1 -0
  35. package/dist/sandbox/sandbox-manager.js +666 -0
  36. package/dist/sandbox/sandbox-manager.js.map +1 -0
  37. package/dist/sandbox/sandbox-schemas.d.ts +17 -0
  38. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -0
  39. package/dist/sandbox/sandbox-schemas.js +2 -0
  40. package/dist/sandbox/sandbox-schemas.js.map +1 -0
  41. package/dist/sandbox/sandbox-utils.d.ts +53 -0
  42. package/dist/sandbox/sandbox-utils.d.ts.map +1 -0
  43. package/dist/sandbox/sandbox-utils.js +368 -0
  44. package/dist/sandbox/sandbox-utils.js.map +1 -0
  45. package/dist/sandbox/sandbox-violation-store.d.ts +19 -0
  46. package/dist/sandbox/sandbox-violation-store.d.ts.map +1 -0
  47. package/dist/sandbox/sandbox-violation-store.js +54 -0
  48. package/dist/sandbox/sandbox-violation-store.js.map +1 -0
  49. package/dist/sandbox/socks-proxy.d.ts +18 -0
  50. package/dist/sandbox/socks-proxy.d.ts.map +1 -0
  51. package/dist/sandbox/socks-proxy.js +242 -0
  52. package/dist/sandbox/socks-proxy.js.map +1 -0
  53. package/dist/utils/debug.d.ts +7 -0
  54. package/dist/utils/debug.d.ts.map +1 -0
  55. package/dist/utils/debug.js +22 -0
  56. package/dist/utils/debug.js.map +1 -0
  57. package/dist/utils/platform.d.ts +6 -0
  58. package/dist/utils/platform.d.ts.map +1 -0
  59. package/dist/utils/platform.js +16 -0
  60. package/dist/utils/platform.js.map +1 -0
  61. package/dist/utils/ripgrep.d.ts +20 -0
  62. package/dist/utils/ripgrep.d.ts.map +1 -0
  63. package/dist/utils/ripgrep.js +51 -0
  64. package/dist/utils/ripgrep.js.map +1 -0
  65. package/dist/utils/telemetry.d.ts +67 -0
  66. package/dist/utils/telemetry.d.ts.map +1 -0
  67. package/dist/utils/telemetry.js +249 -0
  68. package/dist/utils/telemetry.js.map +1 -0
  69. package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
  70. package/dist/vendor/seccomp/arm64/unix-block.bpf +0 -0
  71. package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
  72. package/dist/vendor/seccomp/x64/unix-block.bpf +0 -0
  73. package/dist/vendor/seccomp-src/apply-seccomp.c +98 -0
  74. package/dist/vendor/seccomp-src/seccomp-unix-block.c +97 -0
  75. package/package.json +80 -0
  76. package/vendor/seccomp/arm64/apply-seccomp +0 -0
  77. package/vendor/seccomp/arm64/unix-block.bpf +0 -0
  78. package/vendor/seccomp/x64/apply-seccomp +0 -0
  79. package/vendor/seccomp/x64/unix-block.bpf +0 -0
  80. package/vendor/seccomp-src/apply-seccomp.c +98 -0
  81. package/vendor/seccomp-src/seccomp-unix-block.c +97 -0
@@ -0,0 +1,158 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as fs from 'node:fs';
4
+ import { logForDebugging } from '../utils/debug.js';
5
+ /**
6
+ * Map Node.js process.arch to our vendor directory architecture names
7
+ * Returns null for unsupported architectures
8
+ */
9
+ function getVendorArchitecture() {
10
+ const arch = process.arch;
11
+ switch (arch) {
12
+ case 'x64':
13
+ case 'x86_64':
14
+ return 'x64';
15
+ case 'arm64':
16
+ case 'aarch64':
17
+ return 'arm64';
18
+ case 'ia32':
19
+ case 'x86':
20
+ // TODO: Add support for 32-bit x86 (ia32)
21
+ // Currently blocked because the seccomp filter does not block the socketcall() syscall,
22
+ // which is used on 32-bit x86 for all socket operations (socket, socketpair, bind, connect, etc.).
23
+ // On 32-bit x86, the direct socket() syscall doesn't exist - instead, all socket operations
24
+ // are multiplexed through socketcall(SYS_SOCKET, ...), socketcall(SYS_SOCKETPAIR, ...), etc.
25
+ //
26
+ // To properly support 32-bit x86, we need to:
27
+ // 1. Build a separate i386 BPF filter (BPF bytecode is architecture-specific)
28
+ // 2. Modify vendor/seccomp-src/seccomp-unix-block.c to conditionally add rules that block:
29
+ // - socketcall(SYS_SOCKET, [AF_UNIX, ...])
30
+ // - socketcall(SYS_SOCKETPAIR, [AF_UNIX, ...])
31
+ // 3. This requires complex BPF logic to inspect socketcall's sub-function argument
32
+ //
33
+ // Until then, 32-bit x86 is not supported to avoid a security bypass.
34
+ logForDebugging(`[SeccompFilter] 32-bit x86 (ia32) is not currently supported due to missing socketcall() syscall blocking. ` +
35
+ `The current seccomp filter only blocks socket(AF_UNIX, ...), but on 32-bit x86, socketcall() can be used to bypass this.`, { level: 'error' });
36
+ return null;
37
+ default:
38
+ logForDebugging(`[SeccompFilter] Unsupported architecture: ${arch}. Only x64 and arm64 are supported.`);
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Get the path to a pre-generated BPF filter file from the vendor directory
44
+ * Returns the path if it exists, null otherwise
45
+ *
46
+ * Pre-generated BPF files are organized by architecture:
47
+ * - vendor/seccomp/{x64,arm64}/unix-block.bpf
48
+ *
49
+ * Tries multiple paths for resilience:
50
+ * 1. ../../vendor/seccomp/{arch}/unix-block.bpf (package root - standard npm installs)
51
+ * 2. ../vendor/seccomp/{arch}/unix-block.bpf (dist/vendor - for bundlers)
52
+ */
53
+ export function getPreGeneratedBpfPath() {
54
+ // Determine architecture
55
+ const arch = getVendorArchitecture();
56
+ if (!arch) {
57
+ logForDebugging(`[SeccompFilter] Cannot find pre-generated BPF filter: unsupported architecture ${process.arch}`);
58
+ return null;
59
+ }
60
+ logForDebugging(`[SeccompFilter] Detected architecture: ${arch}`);
61
+ // Try to locate the BPF file with fallback paths
62
+ // Path is relative to the compiled code location (dist/sandbox/)
63
+ const baseDir = dirname(fileURLToPath(import.meta.url));
64
+ const relativePath = join('vendor', 'seccomp', arch, 'unix-block.bpf');
65
+ // Try paths in order of preference
66
+ const pathsToTry = [
67
+ join(baseDir, '..', '..', relativePath), // package root: vendor/seccomp/...
68
+ join(baseDir, '..', relativePath), // dist: dist/vendor/seccomp/...
69
+ ];
70
+ for (const bpfPath of pathsToTry) {
71
+ if (fs.existsSync(bpfPath)) {
72
+ logForDebugging(`[SeccompFilter] Found pre-generated BPF filter: ${bpfPath} (${arch})`);
73
+ return bpfPath;
74
+ }
75
+ }
76
+ logForDebugging(`[SeccompFilter] Pre-generated BPF filter not found in any expected location (${arch})`);
77
+ return null;
78
+ }
79
+ /**
80
+ * Get the path to the apply-seccomp binary from the vendor directory
81
+ * Returns the path if it exists, null otherwise
82
+ *
83
+ * Pre-built apply-seccomp binaries are organized by architecture:
84
+ * - vendor/seccomp/{x64,arm64}/apply-seccomp
85
+ *
86
+ * Tries multiple paths for resilience:
87
+ * 1. ../../vendor/seccomp/{arch}/apply-seccomp (package root - standard npm installs)
88
+ * 2. ../vendor/seccomp/{arch}/apply-seccomp (dist/vendor - for bundlers)
89
+ */
90
+ export function getApplySeccompBinaryPath() {
91
+ // Determine architecture
92
+ const arch = getVendorArchitecture();
93
+ if (!arch) {
94
+ logForDebugging(`[SeccompFilter] Cannot find apply-seccomp binary: unsupported architecture ${process.arch}`);
95
+ return null;
96
+ }
97
+ logForDebugging(`[SeccompFilter] Looking for apply-seccomp binary for architecture: ${arch}`);
98
+ // Try to locate the binary with fallback paths
99
+ // Path is relative to the compiled code location (dist/sandbox/)
100
+ const baseDir = dirname(fileURLToPath(import.meta.url));
101
+ const relativePath = join('vendor', 'seccomp', arch, 'apply-seccomp');
102
+ // Try paths in order of preference
103
+ const pathsToTry = [
104
+ join(baseDir, '..', '..', relativePath), // package root: vendor/seccomp/...
105
+ join(baseDir, '..', relativePath), // dist: dist/vendor/seccomp/...
106
+ ];
107
+ for (const binaryPath of pathsToTry) {
108
+ if (fs.existsSync(binaryPath)) {
109
+ logForDebugging(`[SeccompFilter] Found apply-seccomp binary: ${binaryPath} (${arch})`);
110
+ return binaryPath;
111
+ }
112
+ }
113
+ logForDebugging(`[SeccompFilter] apply-seccomp binary not found in any expected location (${arch})`);
114
+ return null;
115
+ }
116
+ /**
117
+ * Get the path to a pre-generated seccomp BPF filter that blocks Unix domain socket creation
118
+ * Returns the path to the BPF filter file, or null if not available
119
+ *
120
+ * The filter blocks socket(AF_UNIX, ...) syscalls while allowing all other syscalls.
121
+ * This prevents creation of new Unix domain socket file descriptors.
122
+ *
123
+ * Security scope:
124
+ * - Blocks: socket(AF_UNIX, ...) syscall (creating new Unix socket FDs)
125
+ * - Does NOT block: Operations on inherited Unix socket FDs (bind, connect, sendto, etc.)
126
+ * - Does NOT block: Unix socket FDs passed via SCM_RIGHTS
127
+ * - For most sandboxing scenarios, blocking socket creation is sufficient
128
+ *
129
+ * Note: This blocks ALL Unix socket creation, regardless of path. The allowUnixSockets
130
+ * configuration is not supported on Linux due to seccomp-bpf limitations (it cannot
131
+ * read user-space memory to inspect socket paths).
132
+ *
133
+ * Requirements:
134
+ * - Pre-generated BPF filters included for x64 and ARM64 only
135
+ * - Other architectures are not supported
136
+ *
137
+ * @returns Path to the pre-generated BPF filter file, or null if not available
138
+ */
139
+ export function generateSeccompFilter() {
140
+ const preGeneratedBpf = getPreGeneratedBpfPath();
141
+ if (preGeneratedBpf) {
142
+ logForDebugging('[SeccompFilter] Using pre-generated BPF filter');
143
+ return preGeneratedBpf;
144
+ }
145
+ logForDebugging('[SeccompFilter] Pre-generated BPF filter not available for this architecture. ' +
146
+ 'Only x64 and arm64 are supported.', { level: 'error' });
147
+ return null;
148
+ }
149
+ /**
150
+ * Clean up a seccomp filter file
151
+ * Since we only use pre-generated BPF files from vendor/, this is a no-op.
152
+ * Pre-generated files are never deleted.
153
+ * Kept for backward compatibility with existing code that calls it.
154
+ */
155
+ export function cleanupSeccompFilter(_filterPath) {
156
+ // No-op: pre-generated BPF files are never cleaned up
157
+ }
158
+ //# sourceMappingURL=generate-seccomp-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-seccomp-filter.js","sourceRoot":"","sources":["../../src/sandbox/generate-seccomp-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Socket, Server } from 'node:net';
2
+ import type { Duplex } from 'node:stream';
3
+ import { type TelemetrySandboxVerdict } from '../utils/telemetry.js';
4
+ export interface ProxyFilterDecision {
5
+ allowed: boolean;
6
+ verdict: TelemetrySandboxVerdict;
7
+ }
8
+ export interface HttpProxyServerOptions {
9
+ filter(port: number, host: string, socket: Socket | Duplex): Promise<ProxyFilterDecision> | ProxyFilterDecision;
10
+ }
11
+ export declare function createHttpProxyServer(options: HttpProxyServerOptions): Server;
12
+ //# sourceMappingURL=http-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-proxy.d.ts","sourceRoot":"","sources":["../../src/sandbox/http-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAUzC,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAE9B,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,uBAAuB,CAAA;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CACJ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAAA;CACtD;AAsCD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CA+f7E"}
@@ -0,0 +1,489 @@
1
+ import { createServer } from 'node:http';
2
+ import { request as httpRequest } from 'node:http';
3
+ import { request as httpsRequest } from 'node:https';
4
+ import { connect } from 'node:net';
5
+ import { URL } from 'node:url';
6
+ import { performance } from 'node:perf_hooks';
7
+ import { createHash } from 'node:crypto';
8
+ import { logForDebugging } from '../utils/debug.js';
9
+ import { emitTelemetryEvent, isTelemetryEnabled, sanitizeHeaders, } from '../utils/telemetry.js';
10
+ const HTTP_FORBIDDEN_RESPONSE = 'HTTP/1.1 403 Forbidden\r\n' +
11
+ 'Content-Type: text/plain\r\n' +
12
+ 'X-Proxy-Error: blocked-by-allowlist\r\n' +
13
+ '\r\n' +
14
+ 'Connection blocked by network allowlist';
15
+ function inferEgressType(port, protocol) {
16
+ if (protocol === 'https:') {
17
+ return 'https';
18
+ }
19
+ if (protocol === 'http:') {
20
+ return 'http';
21
+ }
22
+ if (port === 443) {
23
+ return 'https';
24
+ }
25
+ if (port === 80) {
26
+ return 'http';
27
+ }
28
+ return 'tcp';
29
+ }
30
+ function getCompressionHeader(headers) {
31
+ const value = headers['content-encoding'];
32
+ if (!value) {
33
+ return null;
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return value[0] ?? null;
37
+ }
38
+ return String(value);
39
+ }
40
+ export function createHttpProxyServer(options) {
41
+ const server = createServer();
42
+ server.on('connect', async (req, socket) => {
43
+ socket.on('error', err => {
44
+ logForDebugging(`Client socket error: ${err.message}`, { level: 'error' });
45
+ });
46
+ try {
47
+ const [hostname, portStr] = req.url.split(':');
48
+ const port = portStr === undefined ? undefined : parseInt(portStr, 10);
49
+ if (!hostname || !port || Number.isNaN(port)) {
50
+ logForDebugging(`Invalid CONNECT request: ${req.url}`, {
51
+ level: 'error',
52
+ });
53
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
54
+ return;
55
+ }
56
+ const telemetryActive = isTelemetryEnabled();
57
+ const egressType = inferEgressType(port, 'https:');
58
+ const startTime = telemetryActive ? performance.now() : 0;
59
+ if (telemetryActive) {
60
+ emitTelemetryEvent({
61
+ stage: 'start',
62
+ status: 'success',
63
+ attempt: 0,
64
+ sandbox_verdict: {
65
+ decision: 'pending',
66
+ reason: 'connect_request_received',
67
+ policy_tag: 'network.allowlist',
68
+ },
69
+ network: {
70
+ resolved_host: hostname,
71
+ resolved_ip: null,
72
+ tls_outcome: null,
73
+ tls_error: null,
74
+ dns_source: null,
75
+ },
76
+ egress_type: egressType,
77
+ });
78
+ }
79
+ let decision;
80
+ try {
81
+ decision = await options.filter(port, hostname, socket);
82
+ }
83
+ catch (error) {
84
+ logForDebugging(`Filter threw error: ${error}`, { level: 'error' });
85
+ decision = {
86
+ allowed: false,
87
+ verdict: {
88
+ decision: 'deny',
89
+ reason: 'filter_failure',
90
+ policy_tag: 'network.allowlist',
91
+ },
92
+ };
93
+ }
94
+ if (!decision.allowed) {
95
+ if (telemetryActive) {
96
+ emitTelemetryEvent({
97
+ stage: 'failure',
98
+ status: 'failed',
99
+ attempt: 0,
100
+ status_code: 403,
101
+ sandbox_verdict: decision.verdict,
102
+ network: {
103
+ resolved_host: hostname,
104
+ resolved_ip: null,
105
+ tls_outcome: null,
106
+ tls_error: null,
107
+ dns_source: null,
108
+ },
109
+ latency_ms: performance.now() - startTime,
110
+ egress_type: egressType,
111
+ });
112
+ }
113
+ socket.end(HTTP_FORBIDDEN_RESPONSE);
114
+ return;
115
+ }
116
+ if (telemetryActive) {
117
+ emitTelemetryEvent({
118
+ stage: 'attempt',
119
+ status: 'success',
120
+ attempt: 0,
121
+ sandbox_verdict: decision.verdict,
122
+ network: {
123
+ resolved_host: hostname,
124
+ resolved_ip: null,
125
+ tls_outcome: null,
126
+ tls_error: null,
127
+ dns_source: null,
128
+ },
129
+ latency_ms: null,
130
+ queue_latency_ms: performance.now() - startTime,
131
+ egress_type: egressType,
132
+ });
133
+ }
134
+ const attemptStart = telemetryActive ? performance.now() : 0;
135
+ let resolvedIp = null;
136
+ let completionEmitted = false;
137
+ let tlsError = null;
138
+ const serverSocket = connect(port, hostname, () => {
139
+ socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
140
+ serverSocket.pipe(socket);
141
+ socket.pipe(serverSocket);
142
+ if (telemetryActive && !completionEmitted) {
143
+ completionEmitted = true;
144
+ emitTelemetryEvent({
145
+ stage: 'completion',
146
+ status: 'success',
147
+ attempt: 0,
148
+ status_code: 200,
149
+ sandbox_verdict: decision.verdict,
150
+ network: {
151
+ resolved_host: hostname,
152
+ resolved_ip: resolvedIp ?? serverSocket.remoteAddress ?? null,
153
+ tls_outcome: 'tunnel_established',
154
+ tls_error: tlsError,
155
+ dns_source: resolvedIp ? 'system' : null,
156
+ },
157
+ latency_ms: performance.now() - attemptStart,
158
+ egress_type: egressType,
159
+ });
160
+ }
161
+ });
162
+ serverSocket.once('lookup', (err, address) => {
163
+ if (err) {
164
+ tlsError = err.message;
165
+ return;
166
+ }
167
+ resolvedIp = address;
168
+ });
169
+ serverSocket.on('error', err => {
170
+ logForDebugging(`CONNECT tunnel failed: ${err.message}`, {
171
+ level: 'error',
172
+ });
173
+ if (telemetryActive && !completionEmitted) {
174
+ completionEmitted = true;
175
+ emitTelemetryEvent({
176
+ stage: 'failure',
177
+ status: 'failed',
178
+ attempt: 0,
179
+ status_code: 502,
180
+ sandbox_verdict: decision.verdict,
181
+ network: {
182
+ resolved_host: hostname,
183
+ resolved_ip: resolvedIp,
184
+ tls_outcome: 'tunnel_error',
185
+ tls_error: err.message,
186
+ dns_source: resolvedIp ? 'system' : null,
187
+ },
188
+ latency_ms: performance.now() - attemptStart,
189
+ egress_type: egressType,
190
+ });
191
+ }
192
+ socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n');
193
+ });
194
+ socket.on('error', err => {
195
+ logForDebugging(`Client socket error: ${err.message}`, {
196
+ level: 'error',
197
+ });
198
+ serverSocket.destroy();
199
+ if (telemetryActive && !completionEmitted) {
200
+ completionEmitted = true;
201
+ emitTelemetryEvent({
202
+ stage: 'failure',
203
+ status: 'failed',
204
+ attempt: 0,
205
+ status_code: null,
206
+ sandbox_verdict: decision.verdict,
207
+ network: {
208
+ resolved_host: hostname,
209
+ resolved_ip: resolvedIp,
210
+ tls_outcome: 'client_socket_error',
211
+ tls_error: err.message,
212
+ dns_source: resolvedIp ? 'system' : null,
213
+ },
214
+ latency_ms: performance.now() - attemptStart,
215
+ egress_type: egressType,
216
+ });
217
+ }
218
+ });
219
+ socket.on('end', () => serverSocket.end());
220
+ serverSocket.on('end', () => socket.end());
221
+ }
222
+ catch (err) {
223
+ logForDebugging(`Error handling CONNECT: ${err}`, { level: 'error' });
224
+ socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n');
225
+ }
226
+ });
227
+ server.on('request', async (req, res) => {
228
+ const telemetryActive = isTelemetryEnabled();
229
+ const startTime = telemetryActive ? performance.now() : 0;
230
+ try {
231
+ const url = new URL(req.url);
232
+ const hostname = url.hostname;
233
+ const port = url.port && url.port !== ''
234
+ ? parseInt(url.port, 10)
235
+ : url.protocol === 'https:'
236
+ ? 443
237
+ : 80;
238
+ const egressType = inferEgressType(port, url.protocol);
239
+ if (telemetryActive) {
240
+ emitTelemetryEvent({
241
+ stage: 'start',
242
+ status: 'success',
243
+ attempt: 0,
244
+ sandbox_verdict: {
245
+ decision: 'pending',
246
+ reason: 'http_request_received',
247
+ policy_tag: 'network.allowlist',
248
+ },
249
+ network: {
250
+ resolved_host: hostname,
251
+ resolved_ip: null,
252
+ tls_outcome: null,
253
+ tls_error: null,
254
+ dns_source: null,
255
+ },
256
+ egress_type: egressType,
257
+ });
258
+ }
259
+ let decision;
260
+ try {
261
+ decision = await options.filter(port, hostname, req.socket);
262
+ }
263
+ catch (error) {
264
+ logForDebugging(`Filter threw error: ${error}`, { level: 'error' });
265
+ decision = {
266
+ allowed: false,
267
+ verdict: {
268
+ decision: 'deny',
269
+ reason: 'filter_failure',
270
+ policy_tag: 'network.allowlist',
271
+ },
272
+ };
273
+ }
274
+ if (!decision.allowed) {
275
+ if (telemetryActive) {
276
+ emitTelemetryEvent({
277
+ stage: 'failure',
278
+ status: 'failed',
279
+ attempt: 0,
280
+ status_code: 403,
281
+ sandbox_verdict: decision.verdict,
282
+ network: {
283
+ resolved_host: hostname,
284
+ resolved_ip: null,
285
+ tls_outcome: null,
286
+ tls_error: null,
287
+ dns_source: null,
288
+ },
289
+ latency_ms: performance.now() - startTime,
290
+ egress_type: egressType,
291
+ });
292
+ }
293
+ res.writeHead(403, {
294
+ 'Content-Type': 'text/plain',
295
+ 'X-Proxy-Error': 'blocked-by-allowlist',
296
+ });
297
+ res.end('Connection blocked by network allowlist');
298
+ return;
299
+ }
300
+ if (telemetryActive) {
301
+ emitTelemetryEvent({
302
+ stage: 'attempt',
303
+ status: 'success',
304
+ attempt: 0,
305
+ sandbox_verdict: decision.verdict,
306
+ network: {
307
+ resolved_host: hostname,
308
+ resolved_ip: null,
309
+ tls_outcome: null,
310
+ tls_error: null,
311
+ dns_source: null,
312
+ },
313
+ latency_ms: null,
314
+ queue_latency_ms: performance.now() - startTime,
315
+ egress_type: egressType,
316
+ });
317
+ }
318
+ const attemptStart = telemetryActive ? performance.now() : 0;
319
+ let resolvedIp = null;
320
+ let tlsOutcome = url.protocol === 'https:' ? 'handshake_pending' : null;
321
+ let tlsError = null;
322
+ let telemetryCompleted = false;
323
+ const requestHeaders = telemetryActive
324
+ ? sanitizeHeaders(req.headers)
325
+ : {};
326
+ const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest;
327
+ const proxyReq = requestFn({
328
+ hostname,
329
+ port,
330
+ path: url.pathname + url.search,
331
+ method: req.method,
332
+ headers: {
333
+ ...req.headers,
334
+ host: url.host,
335
+ },
336
+ }, proxyRes => {
337
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
338
+ const responseHeaders = telemetryActive
339
+ ? sanitizeHeaders(proxyRes.headers)
340
+ : null;
341
+ let responseBytes = 0;
342
+ const hash = telemetryActive ? createHash('sha256') : null;
343
+ if (telemetryActive && hash) {
344
+ proxyRes.on('data', chunk => {
345
+ responseBytes += chunk.length;
346
+ hash.update(chunk);
347
+ });
348
+ }
349
+ proxyRes.on('end', () => {
350
+ if (telemetryActive && !telemetryCompleted) {
351
+ telemetryCompleted = true;
352
+ const latency = performance.now() - attemptStart;
353
+ const payloadHash = hash && responseBytes > 0 ? hash.digest('hex') : null;
354
+ emitTelemetryEvent({
355
+ stage: 'completion',
356
+ status: proxyRes.statusCode &&
357
+ proxyRes.statusCode >= 200 &&
358
+ proxyRes.statusCode < 400
359
+ ? 'success'
360
+ : 'failed',
361
+ attempt: 0,
362
+ status_code: proxyRes.statusCode ?? null,
363
+ sandbox_verdict: decision.verdict,
364
+ network: {
365
+ resolved_host: hostname,
366
+ resolved_ip: resolvedIp ??
367
+ (proxyRes.socket
368
+ ? (proxyRes.socket.remoteAddress ?? null)
369
+ : null),
370
+ tls_outcome: tlsOutcome,
371
+ tls_error: tlsError,
372
+ dns_source: resolvedIp ? 'system' : null,
373
+ },
374
+ http_metadata: {
375
+ req_headers: requestHeaders,
376
+ resp_headers: responseHeaders,
377
+ payload_bytes: responseBytes,
378
+ payload_hash: payloadHash,
379
+ compression: responseHeaders
380
+ ? getCompressionHeader(proxyRes.headers)
381
+ : null,
382
+ },
383
+ latency_ms: latency,
384
+ egress_type: egressType,
385
+ });
386
+ }
387
+ });
388
+ proxyRes.on('error', err => {
389
+ logForDebugging(`Proxy response error: ${err.message}`, {
390
+ level: 'error',
391
+ });
392
+ if (telemetryActive && !telemetryCompleted) {
393
+ telemetryCompleted = true;
394
+ emitTelemetryEvent({
395
+ stage: 'failure',
396
+ status: 'failed',
397
+ attempt: 0,
398
+ status_code: null,
399
+ sandbox_verdict: decision.verdict,
400
+ network: {
401
+ resolved_host: hostname,
402
+ resolved_ip: resolvedIp,
403
+ tls_outcome: tlsOutcome,
404
+ tls_error: err.message,
405
+ dns_source: resolvedIp ? 'system' : null,
406
+ },
407
+ http_metadata: {
408
+ req_headers: requestHeaders,
409
+ resp_headers: responseHeaders,
410
+ payload_bytes: null,
411
+ payload_hash: null,
412
+ compression: null,
413
+ },
414
+ latency_ms: performance.now() - attemptStart,
415
+ egress_type: egressType,
416
+ });
417
+ }
418
+ });
419
+ proxyRes.pipe(res);
420
+ });
421
+ proxyReq.on('socket', (socket) => {
422
+ if (!telemetryActive) {
423
+ return;
424
+ }
425
+ socket.once('lookup', (err, address) => {
426
+ if (err) {
427
+ tlsError = err.message;
428
+ return;
429
+ }
430
+ resolvedIp = address;
431
+ });
432
+ socket.once('error', err => {
433
+ tlsError = err.message;
434
+ });
435
+ if (url.protocol === 'https:') {
436
+ ;
437
+ socket.once('secureConnect', () => {
438
+ tlsOutcome = 'handshake_success';
439
+ });
440
+ }
441
+ });
442
+ proxyReq.on('error', err => {
443
+ logForDebugging(`Proxy request failed: ${err.message}`, {
444
+ level: 'error',
445
+ });
446
+ if (telemetryActive && !telemetryCompleted) {
447
+ telemetryCompleted = true;
448
+ emitTelemetryEvent({
449
+ stage: 'failure',
450
+ status: 'failed',
451
+ attempt: 0,
452
+ status_code: null,
453
+ sandbox_verdict: decision.verdict,
454
+ network: {
455
+ resolved_host: hostname,
456
+ resolved_ip: resolvedIp,
457
+ tls_outcome: url.protocol === 'https:'
458
+ ? 'handshake_error'
459
+ : 'connection_error',
460
+ tls_error: err.message,
461
+ dns_source: resolvedIp ? 'system' : null,
462
+ },
463
+ http_metadata: {
464
+ req_headers: requestHeaders,
465
+ resp_headers: null,
466
+ payload_bytes: null,
467
+ payload_hash: null,
468
+ compression: null,
469
+ },
470
+ latency_ms: performance.now() - attemptStart,
471
+ egress_type: egressType,
472
+ });
473
+ }
474
+ if (!res.headersSent) {
475
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
476
+ res.end('Bad Gateway');
477
+ }
478
+ });
479
+ req.pipe(proxyReq);
480
+ }
481
+ catch (err) {
482
+ logForDebugging(`Error handling HTTP request: ${err}`, { level: 'error' });
483
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
484
+ res.end('Internal Server Error');
485
+ }
486
+ });
487
+ return server;
488
+ }
489
+ //# sourceMappingURL=http-proxy.js.map