smartcontext-proxy 0.1.0 → 0.2.1

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 (64) hide show
  1. package/PLAN-v2.md +390 -0
  2. package/dist/src/context/ab-test.d.ts +32 -0
  3. package/dist/src/context/ab-test.js +133 -0
  4. package/dist/src/index.js +99 -78
  5. package/dist/src/proxy/classifier.d.ts +14 -0
  6. package/dist/src/proxy/classifier.js +63 -0
  7. package/dist/src/proxy/connect-proxy.d.ts +37 -0
  8. package/dist/src/proxy/connect-proxy.js +234 -0
  9. package/dist/src/proxy/server.js +10 -1
  10. package/dist/src/proxy/tls-interceptor.d.ts +23 -0
  11. package/dist/src/proxy/tls-interceptor.js +211 -0
  12. package/dist/src/proxy/transparent-listener.d.ts +31 -0
  13. package/dist/src/proxy/transparent-listener.js +285 -0
  14. package/dist/src/proxy/tunnel.d.ts +7 -0
  15. package/dist/src/proxy/tunnel.js +33 -0
  16. package/dist/src/system/dns-redirect.d.ts +28 -0
  17. package/dist/src/system/dns-redirect.js +141 -0
  18. package/dist/src/system/installer.d.ts +25 -0
  19. package/dist/src/system/installer.js +180 -0
  20. package/dist/src/system/linux.d.ts +11 -0
  21. package/dist/src/system/linux.js +60 -0
  22. package/dist/src/system/macos.d.ts +24 -0
  23. package/dist/src/system/macos.js +98 -0
  24. package/dist/src/system/pf-redirect.d.ts +25 -0
  25. package/dist/src/system/pf-redirect.js +177 -0
  26. package/dist/src/system/watchdog.d.ts +7 -0
  27. package/dist/src/system/watchdog.js +115 -0
  28. package/dist/src/test/connect-proxy.test.d.ts +1 -0
  29. package/dist/src/test/connect-proxy.test.js +147 -0
  30. package/dist/src/test/dashboard.test.js +1 -0
  31. package/dist/src/tls/ca-manager.d.ts +9 -0
  32. package/dist/src/tls/ca-manager.js +117 -0
  33. package/dist/src/tls/trust-store.d.ts +11 -0
  34. package/dist/src/tls/trust-store.js +121 -0
  35. package/dist/src/tray/bridge.d.ts +8 -0
  36. package/dist/src/tray/bridge.js +66 -0
  37. package/dist/src/ui/dashboard.d.ts +10 -1
  38. package/dist/src/ui/dashboard.js +119 -34
  39. package/dist/src/ui/ws-feed.d.ts +8 -0
  40. package/dist/src/ui/ws-feed.js +30 -0
  41. package/native/macos/SmartContextTray/Package.swift +13 -0
  42. package/native/macos/SmartContextTray/Sources/main.swift +206 -0
  43. package/package.json +6 -2
  44. package/src/context/ab-test.ts +172 -0
  45. package/src/index.ts +104 -74
  46. package/src/proxy/classifier.ts +71 -0
  47. package/src/proxy/connect-proxy.ts +251 -0
  48. package/src/proxy/server.ts +11 -2
  49. package/src/proxy/tls-interceptor.ts +261 -0
  50. package/src/proxy/transparent-listener.ts +328 -0
  51. package/src/proxy/tunnel.ts +32 -0
  52. package/src/system/dns-redirect.ts +144 -0
  53. package/src/system/installer.ts +148 -0
  54. package/src/system/linux.ts +57 -0
  55. package/src/system/macos.ts +89 -0
  56. package/src/system/pf-redirect.ts +175 -0
  57. package/src/system/watchdog.ts +76 -0
  58. package/src/test/connect-proxy.test.ts +170 -0
  59. package/src/test/dashboard.test.ts +1 -0
  60. package/src/tls/ca-manager.ts +140 -0
  61. package/src/tls/trust-store.ts +123 -0
  62. package/src/tray/bridge.ts +61 -0
  63. package/src/ui/dashboard.ts +129 -35
  64. package/src/ui/ws-feed.ts +32 -0
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.interceptTLS = interceptTLS;
7
+ const node_tls_1 = __importDefault(require("node:tls"));
8
+ const node_http_1 = __importDefault(require("node:http"));
9
+ const node_https_1 = __importDefault(require("node:https"));
10
+ const ca_manager_js_1 = require("../tls/ca-manager.js");
11
+ const chunker_js_1 = require("../context/chunker.js");
12
+ const canonical_js_1 = require("../context/canonical.js");
13
+ /**
14
+ * Intercept TLS connection to an LLM provider.
15
+ * Terminates TLS with a generated cert, parses the HTTP request inside,
16
+ * optionally optimizes context, then forwards to the real provider.
17
+ */
18
+ function interceptTLS(clientSocket, hostname, port, match, options) {
19
+ const { cert, key } = (0, ca_manager_js_1.getCertForHost)(hostname);
20
+ const tlsSocket = new node_tls_1.default.TLSSocket(clientSocket, {
21
+ isServer: true,
22
+ cert,
23
+ key,
24
+ });
25
+ // Tell client the tunnel is established
26
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
27
+ // Read HTTP request from decrypted TLS stream
28
+ let requestData = Buffer.alloc(0);
29
+ let headersParsed = false;
30
+ let contentLength = 0;
31
+ let headerEnd = -1;
32
+ tlsSocket.on('data', (chunk) => {
33
+ requestData = Buffer.concat([requestData, chunk]);
34
+ if (!headersParsed) {
35
+ headerEnd = requestData.indexOf('\r\n\r\n');
36
+ if (headerEnd === -1)
37
+ return; // Wait for more data
38
+ headersParsed = true;
39
+ const headersStr = requestData.subarray(0, headerEnd).toString();
40
+ const clMatch = headersStr.match(/content-length:\s*(\d+)/i);
41
+ contentLength = clMatch ? parseInt(clMatch[1], 10) : 0;
42
+ }
43
+ const bodyStart = headerEnd + 4;
44
+ const bodyReceived = requestData.length - bodyStart;
45
+ if (bodyReceived >= contentLength) {
46
+ tlsSocket.pause();
47
+ handleInterceptedRequest(requestData, hostname, port, match, options, tlsSocket).catch((err) => {
48
+ options.log('error', `Intercept error: ${err}`);
49
+ try {
50
+ tlsSocket.write('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
51
+ tlsSocket.end();
52
+ }
53
+ catch { }
54
+ });
55
+ }
56
+ });
57
+ tlsSocket.on('error', (err) => {
58
+ options.log('error', `TLS socket error for ${hostname}: ${err.message}`);
59
+ });
60
+ }
61
+ /**
62
+ * Handle an intercepted HTTP request: parse, optimize, forward, stream back.
63
+ */
64
+ async function handleInterceptedRequest(rawRequest, hostname, port, match, options, clientTLS) {
65
+ const startTime = Date.now();
66
+ options.requestCounter.value++;
67
+ const reqId = options.requestCounter.value;
68
+ // Parse raw HTTP request
69
+ const headerEnd = rawRequest.indexOf('\r\n\r\n');
70
+ const headersStr = rawRequest.subarray(0, headerEnd).toString();
71
+ const body = rawRequest.subarray(headerEnd + 4);
72
+ const [requestLine, ...headerLines] = headersStr.split('\r\n');
73
+ const [method, path] = requestLine.split(' ');
74
+ const headers = {};
75
+ for (const line of headerLines) {
76
+ const colonIdx = line.indexOf(':');
77
+ if (colonIdx > 0) {
78
+ headers[line.substring(0, colonIdx).toLowerCase().trim()] = line.substring(colonIdx + 1).trim();
79
+ }
80
+ }
81
+ // Find adapter for this provider
82
+ const adapter = options.adapters.get(match.provider);
83
+ let forwardBody;
84
+ let originalTokens = 0;
85
+ let optimizedTokens = 0;
86
+ let savingsPercent = 0;
87
+ let chunksRetrieved = 0;
88
+ let topScore = 0;
89
+ let passThrough = true;
90
+ let reason;
91
+ let model = 'unknown';
92
+ if (adapter && body.length > 0) {
93
+ try {
94
+ const parsed = JSON.parse(body.toString());
95
+ model = parsed.model || 'unknown';
96
+ const canonical = adapter.parseRequest(parsed, headers);
97
+ originalTokens = (0, chunker_js_1.estimateTokens)(canonical.systemPrompt || '') +
98
+ canonical.messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
99
+ // Optimize if available and not paused
100
+ if (options.optimizer && !options.paused) {
101
+ try {
102
+ const result = await options.optimizer.optimize(canonical);
103
+ passThrough = result.passThrough;
104
+ reason = result.reason;
105
+ if (!result.passThrough) {
106
+ canonical.messages = result.optimizedMessages;
107
+ if (result.systemPrompt !== undefined)
108
+ canonical.systemPrompt = result.systemPrompt;
109
+ optimizedTokens = result.packed.optimizedTokens;
110
+ savingsPercent = result.packed.savingsPercent;
111
+ }
112
+ if (result.retrieval) {
113
+ chunksRetrieved = result.retrieval.chunks.length;
114
+ topScore = result.retrieval.topScore;
115
+ }
116
+ }
117
+ catch (err) {
118
+ passThrough = true;
119
+ reason = `optimization error: ${err}`;
120
+ }
121
+ }
122
+ if (!passThrough) {
123
+ forwardBody = Buffer.from(JSON.stringify(adapter.serializeRequest(canonical)));
124
+ }
125
+ else {
126
+ forwardBody = body;
127
+ optimizedTokens = originalTokens;
128
+ }
129
+ }
130
+ catch {
131
+ forwardBody = body;
132
+ optimizedTokens = originalTokens;
133
+ }
134
+ }
135
+ else {
136
+ forwardBody = body;
137
+ }
138
+ const latencyOverhead = Date.now() - startTime;
139
+ const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
140
+ options.log('info', `#${reqId} ${match.provider}/${model} ${originalTokens}→${optimizedTokens} ${savingsStr} ${latencyOverhead}ms [intercepted]`);
141
+ // Forward to real provider
142
+ const useHTTPS = port === 443 || hostname.includes('.');
143
+ const transport = useHTTPS ? node_https_1.default : node_http_1.default;
144
+ const forwardUrl = `${useHTTPS ? 'https' : 'http'}://${hostname}:${port}${path}`;
145
+ const forwardHeaders = { ...headers };
146
+ forwardHeaders['host'] = hostname;
147
+ forwardHeaders['content-length'] = String(forwardBody.length);
148
+ const providerRes = await new Promise((resolve, reject) => {
149
+ const proxyReq = transport.request(forwardUrl, {
150
+ method,
151
+ headers: forwardHeaders,
152
+ }, resolve);
153
+ proxyReq.on('error', reject);
154
+ proxyReq.write(forwardBody);
155
+ proxyReq.end();
156
+ });
157
+ // Build response headers
158
+ const resHeaders = [
159
+ `HTTP/1.1 ${providerRes.statusCode} ${providerRes.statusMessage}`,
160
+ ];
161
+ for (const [key, val] of Object.entries(providerRes.headers)) {
162
+ if (val) {
163
+ const values = Array.isArray(val) ? val : [val];
164
+ for (const v of values) {
165
+ resHeaders.push(`${key}: ${v}`);
166
+ }
167
+ }
168
+ }
169
+ resHeaders.push('', '');
170
+ clientTLS.write(resHeaders.join('\r\n'));
171
+ // Stream response body
172
+ await new Promise((resolve) => {
173
+ providerRes.on('data', (chunk) => {
174
+ clientTLS.write(chunk);
175
+ });
176
+ providerRes.on('end', () => {
177
+ clientTLS.end();
178
+ resolve();
179
+ });
180
+ providerRes.on('error', () => {
181
+ clientTLS.end();
182
+ resolve();
183
+ });
184
+ });
185
+ // Record metrics
186
+ options.metrics.record({
187
+ id: reqId,
188
+ timestamp: Date.now(),
189
+ provider: match.provider,
190
+ model,
191
+ streaming: headers['accept']?.includes('text/event-stream') || false,
192
+ originalTokens,
193
+ optimizedTokens,
194
+ savingsPercent,
195
+ latencyOverheadMs: latencyOverhead,
196
+ chunksRetrieved,
197
+ topScore,
198
+ passThrough,
199
+ reason,
200
+ });
201
+ // Async post-indexing
202
+ if (options.optimizer && !passThrough && adapter) {
203
+ try {
204
+ const parsed = JSON.parse(body.toString());
205
+ const canonical = adapter.parseRequest(parsed, headers);
206
+ options.optimizer.indexExchange(canonical.messages, `intercepted-${reqId}`).catch(() => { });
207
+ }
208
+ catch { }
209
+ }
210
+ }
211
+ //# sourceMappingURL=tls-interceptor.js.map
@@ -0,0 +1,31 @@
1
+ import type { ProviderAdapter } from '../providers/types.js';
2
+ import type { SmartContextConfig } from '../config/schema.js';
3
+ import { ContextOptimizer } from '../context/optimizer.js';
4
+ import { MetricsCollector } from '../metrics/collector.js';
5
+ export interface TransparentOptions {
6
+ config: SmartContextConfig;
7
+ optimizer: ContextOptimizer | null;
8
+ metrics: MetricsCollector;
9
+ adapters: Map<string, ProviderAdapter>;
10
+ paused: boolean;
11
+ requestCounter: {
12
+ value: number;
13
+ };
14
+ log: (level: string, message: string) => void;
15
+ }
16
+ /**
17
+ * Transparent TLS listener on port 443.
18
+ * When DNS redirects LLM traffic to 127.0.0.1, clients connect directly
19
+ * via TLS (no CONNECT). This server terminates TLS using SNI to pick
20
+ * the right cert, then handles the request.
21
+ */
22
+ export declare class TransparentListener {
23
+ private server;
24
+ private options;
25
+ constructor(options: TransparentOptions);
26
+ start(port?: number, host?: string): Promise<void>;
27
+ stop(): Promise<void>;
28
+ private handleConnection;
29
+ private handleTLSConnection;
30
+ private handleRequest;
31
+ }
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TransparentListener = void 0;
7
+ const node_tls_1 = __importDefault(require("node:tls"));
8
+ const node_net_1 = __importDefault(require("node:net"));
9
+ const node_https_1 = __importDefault(require("node:https"));
10
+ const ca_manager_js_1 = require("../tls/ca-manager.js");
11
+ const classifier_js_1 = require("./classifier.js");
12
+ const dns_redirect_js_1 = require("../system/dns-redirect.js");
13
+ const chunker_js_1 = require("../context/chunker.js");
14
+ const canonical_js_1 = require("../context/canonical.js");
15
+ /**
16
+ * Transparent TLS listener on port 443.
17
+ * When DNS redirects LLM traffic to 127.0.0.1, clients connect directly
18
+ * via TLS (no CONNECT). This server terminates TLS using SNI to pick
19
+ * the right cert, then handles the request.
20
+ */
21
+ class TransparentListener {
22
+ server;
23
+ options;
24
+ constructor(options) {
25
+ this.options = options;
26
+ // Raw TCP server — we need SNI from ClientHello before creating TLS context
27
+ this.server = node_net_1.default.createServer((socket) => this.handleConnection(socket));
28
+ }
29
+ async start(port = 443, host = '127.0.0.1') {
30
+ return new Promise((resolve, reject) => {
31
+ this.server.on('error', (err) => {
32
+ if (err.code === 'EACCES') {
33
+ this.options.log('error', `Port ${port} requires root. Transparent listener disabled.`);
34
+ resolve(); // Non-fatal
35
+ }
36
+ else {
37
+ reject(err);
38
+ }
39
+ });
40
+ this.server.listen(port, host, () => {
41
+ this.options.log('info', `Transparent TLS listener on ${host}:${port}`);
42
+ resolve();
43
+ });
44
+ });
45
+ }
46
+ async stop() {
47
+ return new Promise((resolve) => {
48
+ this.server.close(() => resolve());
49
+ });
50
+ }
51
+ handleConnection(socket) {
52
+ // Peek at first bytes to extract SNI from TLS ClientHello
53
+ socket.once('data', (data) => {
54
+ const hostname = extractSNI(data);
55
+ if (!hostname) {
56
+ socket.destroy();
57
+ return;
58
+ }
59
+ const match = (0, classifier_js_1.classifyHost)(hostname, 443);
60
+ if (!match) {
61
+ // Not an LLM host — shouldn't happen with DNS redirect, but just in case
62
+ socket.destroy();
63
+ return;
64
+ }
65
+ this.options.log('info', `TRANSPARENT ${hostname} (${match.provider})`);
66
+ const { cert, key } = (0, ca_manager_js_1.getCertForHost)(hostname);
67
+ const tlsSocket = new node_tls_1.default.TLSSocket(socket, {
68
+ isServer: true,
69
+ cert,
70
+ key,
71
+ });
72
+ // Unshift the peeked data back
73
+ tlsSocket.unshift(data);
74
+ // Actually we already consumed data from socket, TLSSocket wraps socket
75
+ // Need to handle this differently — push data through
76
+ this.handleTLSConnection(tlsSocket, hostname, match);
77
+ });
78
+ }
79
+ handleTLSConnection(tlsSocket, hostname, match) {
80
+ let requestData = Buffer.alloc(0);
81
+ let headersParsed = false;
82
+ let contentLength = 0;
83
+ let headerEnd = -1;
84
+ tlsSocket.on('data', (chunk) => {
85
+ requestData = Buffer.concat([requestData, chunk]);
86
+ if (!headersParsed) {
87
+ headerEnd = requestData.indexOf('\r\n\r\n');
88
+ if (headerEnd === -1)
89
+ return;
90
+ headersParsed = true;
91
+ const headersStr = requestData.subarray(0, headerEnd).toString();
92
+ const clMatch = headersStr.match(/content-length:\s*(\d+)/i);
93
+ contentLength = clMatch ? parseInt(clMatch[1], 10) : 0;
94
+ }
95
+ const bodyStart = headerEnd + 4;
96
+ const bodyReceived = requestData.length - bodyStart;
97
+ if (bodyReceived >= contentLength) {
98
+ tlsSocket.pause();
99
+ this.handleRequest(requestData, hostname, match, tlsSocket).catch((err) => {
100
+ this.options.log('error', `Transparent handler error: ${err}`);
101
+ try {
102
+ tlsSocket.end();
103
+ }
104
+ catch { }
105
+ });
106
+ }
107
+ });
108
+ tlsSocket.on('error', (err) => {
109
+ this.options.log('error', `TLS error for ${hostname}: ${err.message}`);
110
+ });
111
+ }
112
+ async handleRequest(rawRequest, hostname, match, clientTLS) {
113
+ this.options.requestCounter.value++;
114
+ const reqId = this.options.requestCounter.value;
115
+ const startTime = Date.now();
116
+ // Parse HTTP request
117
+ const headerEnd = rawRequest.indexOf('\r\n\r\n');
118
+ const headersStr = rawRequest.subarray(0, headerEnd).toString();
119
+ const body = rawRequest.subarray(headerEnd + 4);
120
+ const [requestLine, ...headerLines] = headersStr.split('\r\n');
121
+ const [method, reqPath] = requestLine.split(' ');
122
+ const headers = {};
123
+ for (const line of headerLines) {
124
+ const colonIdx = line.indexOf(':');
125
+ if (colonIdx > 0) {
126
+ headers[line.substring(0, colonIdx).toLowerCase().trim()] = line.substring(colonIdx + 1).trim();
127
+ }
128
+ }
129
+ // Get real IP for this hostname (cached before DNS override)
130
+ const realIP = (0, dns_redirect_js_1.getRealIP)(hostname);
131
+ if (!realIP) {
132
+ this.options.log('error', `No real IP cached for ${hostname}`);
133
+ clientTLS.write('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
134
+ clientTLS.end();
135
+ return;
136
+ }
137
+ // Optimization logic
138
+ const adapter = this.options.adapters.get(match.provider);
139
+ let forwardBody = body;
140
+ let model = 'unknown';
141
+ let originalTokens = 0;
142
+ let optimizedTokens = 0;
143
+ let savingsPercent = 0;
144
+ let passThrough = true;
145
+ if (adapter && body.length > 0) {
146
+ try {
147
+ const parsed = JSON.parse(body.toString());
148
+ model = parsed.model || 'unknown';
149
+ const canonical = adapter.parseRequest(parsed, headers);
150
+ originalTokens = (0, chunker_js_1.estimateTokens)(canonical.systemPrompt || '') +
151
+ canonical.messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
152
+ if (this.options.optimizer && !this.options.paused) {
153
+ try {
154
+ const result = await this.options.optimizer.optimize(canonical);
155
+ passThrough = result.passThrough;
156
+ if (!result.passThrough) {
157
+ canonical.messages = result.optimizedMessages;
158
+ if (result.systemPrompt !== undefined)
159
+ canonical.systemPrompt = result.systemPrompt;
160
+ optimizedTokens = result.packed.optimizedTokens;
161
+ savingsPercent = result.packed.savingsPercent;
162
+ forwardBody = Buffer.from(JSON.stringify(adapter.serializeRequest(canonical)));
163
+ }
164
+ else {
165
+ optimizedTokens = originalTokens;
166
+ }
167
+ }
168
+ catch {
169
+ optimizedTokens = originalTokens;
170
+ }
171
+ }
172
+ else {
173
+ optimizedTokens = originalTokens;
174
+ }
175
+ }
176
+ catch {
177
+ optimizedTokens = originalTokens;
178
+ }
179
+ }
180
+ const latency = Date.now() - startTime;
181
+ const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
182
+ this.options.log('info', `#${reqId} ${match.provider}/${model} ${originalTokens}→${optimizedTokens} ${savingsStr} ${latency}ms [transparent]`);
183
+ // Forward to real provider IP
184
+ const forwardHeaders = { ...headers };
185
+ forwardHeaders['host'] = hostname;
186
+ forwardHeaders['content-length'] = String(forwardBody.length);
187
+ const providerRes = await new Promise((resolve, reject) => {
188
+ const req = node_https_1.default.request({
189
+ hostname: realIP,
190
+ port: 443,
191
+ path: reqPath,
192
+ method,
193
+ headers: forwardHeaders,
194
+ servername: hostname, // SNI for the real server
195
+ }, resolve);
196
+ req.on('error', reject);
197
+ req.write(forwardBody);
198
+ req.end();
199
+ });
200
+ // Stream response back
201
+ const resHeaders = [`HTTP/1.1 ${providerRes.statusCode} ${providerRes.statusMessage}`];
202
+ for (const [key, val] of Object.entries(providerRes.headers)) {
203
+ if (val) {
204
+ const values = Array.isArray(val) ? val : [val];
205
+ for (const v of values)
206
+ resHeaders.push(`${key}: ${v}`);
207
+ }
208
+ }
209
+ resHeaders.push('', '');
210
+ clientTLS.write(resHeaders.join('\r\n'));
211
+ await new Promise((resolve) => {
212
+ providerRes.on('data', (chunk) => clientTLS.write(chunk));
213
+ providerRes.on('end', () => { clientTLS.end(); resolve(); });
214
+ providerRes.on('error', () => { clientTLS.end(); resolve(); });
215
+ });
216
+ // Record metrics
217
+ this.options.metrics.record({
218
+ id: reqId,
219
+ timestamp: Date.now(),
220
+ provider: match.provider,
221
+ model,
222
+ streaming: headers['accept']?.includes('text/event-stream') || false,
223
+ originalTokens,
224
+ optimizedTokens,
225
+ savingsPercent,
226
+ latencyOverheadMs: latency,
227
+ chunksRetrieved: 0,
228
+ topScore: 0,
229
+ passThrough,
230
+ });
231
+ }
232
+ }
233
+ exports.TransparentListener = TransparentListener;
234
+ /**
235
+ * Extract SNI hostname from TLS ClientHello.
236
+ * Returns null if not found.
237
+ */
238
+ function extractSNI(data) {
239
+ try {
240
+ // TLS record: type=22 (handshake), version, length
241
+ if (data.length < 5 || data[0] !== 0x16)
242
+ return null;
243
+ // Handshake: type=1 (ClientHello)
244
+ let offset = 5;
245
+ if (data[offset] !== 0x01)
246
+ return null;
247
+ // Skip handshake length (3 bytes) + client version (2) + random (32)
248
+ offset += 4 + 2 + 32;
249
+ // Session ID
250
+ const sessionIdLen = data[offset];
251
+ offset += 1 + sessionIdLen;
252
+ // Cipher suites
253
+ const cipherLen = data.readUInt16BE(offset);
254
+ offset += 2 + cipherLen;
255
+ // Compression methods
256
+ const compLen = data[offset];
257
+ offset += 1 + compLen;
258
+ // Extensions
259
+ if (offset + 2 > data.length)
260
+ return null;
261
+ const extLen = data.readUInt16BE(offset);
262
+ offset += 2;
263
+ const extEnd = offset + extLen;
264
+ while (offset + 4 < extEnd && offset < data.length) {
265
+ const extType = data.readUInt16BE(offset);
266
+ const extDataLen = data.readUInt16BE(offset + 2);
267
+ offset += 4;
268
+ if (extType === 0x0000) { // SNI extension
269
+ // Skip SNI list length (2)
270
+ offset += 2;
271
+ const nameType = data[offset];
272
+ offset += 1;
273
+ if (nameType === 0x00) { // hostname
274
+ const nameLen = data.readUInt16BE(offset);
275
+ offset += 2;
276
+ return data.subarray(offset, offset + nameLen).toString('ascii');
277
+ }
278
+ }
279
+ offset += extDataLen;
280
+ }
281
+ }
282
+ catch { }
283
+ return null;
284
+ }
285
+ //# sourceMappingURL=transparent-listener.js.map
@@ -0,0 +1,7 @@
1
+ import type { Socket } from 'node:net';
2
+ /**
3
+ * Create a blind TCP tunnel between client and target.
4
+ * Zero overhead — no inspection, no buffering.
5
+ * Used for non-LLM HTTPS traffic.
6
+ */
7
+ export declare function createTunnel(clientSocket: Socket, targetHost: string, targetPort: number): void;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createTunnel = createTunnel;
7
+ const node_net_1 = __importDefault(require("node:net"));
8
+ /**
9
+ * Create a blind TCP tunnel between client and target.
10
+ * Zero overhead — no inspection, no buffering.
11
+ * Used for non-LLM HTTPS traffic.
12
+ */
13
+ function createTunnel(clientSocket, targetHost, targetPort) {
14
+ const targetSocket = node_net_1.default.connect(targetPort, targetHost, () => {
15
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
16
+ targetSocket.pipe(clientSocket);
17
+ clientSocket.pipe(targetSocket);
18
+ });
19
+ targetSocket.on('error', (err) => {
20
+ clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
21
+ clientSocket.end();
22
+ });
23
+ clientSocket.on('error', () => {
24
+ targetSocket.destroy();
25
+ });
26
+ clientSocket.on('close', () => {
27
+ targetSocket.destroy();
28
+ });
29
+ targetSocket.on('close', () => {
30
+ clientSocket.destroy();
31
+ });
32
+ }
33
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve and cache real IPs before overriding DNS.
3
+ * MUST be called before enableDNSRedirect.
4
+ */
5
+ export declare function cacheRealIPs(): Promise<Map<string, string[]>>;
6
+ /** Get cached real IP for a hostname */
7
+ export declare function getRealIP(hostname: string): string | null;
8
+ /** Get all original IP mappings */
9
+ export declare function getAllRealIPs(): Map<string, string[]>;
10
+ /**
11
+ * Add /etc/hosts entries pointing LLM hostnames to 127.0.0.1.
12
+ * Requires sudo.
13
+ * After this, all apps resolve LLM hosts to localhost → our proxy.
14
+ */
15
+ export declare function enableDNSRedirect(): {
16
+ success: boolean;
17
+ message: string;
18
+ hosts: number;
19
+ };
20
+ /**
21
+ * Remove /etc/hosts entries.
22
+ */
23
+ export declare function disableDNSRedirect(): {
24
+ success: boolean;
25
+ message: string;
26
+ };
27
+ /** Check if DNS redirect is active */
28
+ export declare function isDNSRedirectActive(): boolean;