smartcontext-proxy 0.1.0 → 0.2.0
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.
- package/PLAN-v2.md +390 -0
- package/dist/src/context/ab-test.d.ts +32 -0
- package/dist/src/context/ab-test.js +133 -0
- package/dist/src/index.js +99 -78
- package/dist/src/proxy/classifier.d.ts +14 -0
- package/dist/src/proxy/classifier.js +63 -0
- package/dist/src/proxy/connect-proxy.d.ts +34 -0
- package/dist/src/proxy/connect-proxy.js +167 -0
- package/dist/src/proxy/tls-interceptor.d.ts +23 -0
- package/dist/src/proxy/tls-interceptor.js +211 -0
- package/dist/src/proxy/tunnel.d.ts +7 -0
- package/dist/src/proxy/tunnel.js +33 -0
- package/dist/src/system/installer.d.ts +25 -0
- package/dist/src/system/installer.js +180 -0
- package/dist/src/system/linux.d.ts +11 -0
- package/dist/src/system/linux.js +60 -0
- package/dist/src/system/macos.d.ts +24 -0
- package/dist/src/system/macos.js +98 -0
- package/dist/src/system/watchdog.d.ts +7 -0
- package/dist/src/system/watchdog.js +115 -0
- package/dist/src/test/connect-proxy.test.d.ts +1 -0
- package/dist/src/test/connect-proxy.test.js +147 -0
- package/dist/src/tls/ca-manager.d.ts +9 -0
- package/dist/src/tls/ca-manager.js +117 -0
- package/dist/src/tls/trust-store.d.ts +11 -0
- package/dist/src/tls/trust-store.js +121 -0
- package/dist/src/tray/bridge.d.ts +8 -0
- package/dist/src/tray/bridge.js +66 -0
- package/dist/src/ui/ws-feed.d.ts +8 -0
- package/dist/src/ui/ws-feed.js +30 -0
- package/native/macos/SmartContextTray/Package.swift +13 -0
- package/native/macos/SmartContextTray/Sources/main.swift +206 -0
- package/package.json +6 -2
- package/src/context/ab-test.ts +172 -0
- package/src/index.ts +104 -74
- package/src/proxy/classifier.ts +71 -0
- package/src/proxy/connect-proxy.ts +187 -0
- package/src/proxy/tls-interceptor.ts +261 -0
- package/src/proxy/tunnel.ts +32 -0
- package/src/system/installer.ts +148 -0
- package/src/system/linux.ts +57 -0
- package/src/system/macos.ts +89 -0
- package/src/system/watchdog.ts +76 -0
- package/src/test/connect-proxy.test.ts +170 -0
- package/src/tls/ca-manager.ts +140 -0
- package/src/tls/trust-store.ts +123 -0
- package/src/tray/bridge.ts +61 -0
- 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,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,25 @@
|
|
|
1
|
+
export interface InstallResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
steps: Array<{
|
|
4
|
+
step: string;
|
|
5
|
+
success: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Full installation:
|
|
11
|
+
* 1. Generate CA cert
|
|
12
|
+
* 2. Install CA in trust store
|
|
13
|
+
* 3. Configure system proxy
|
|
14
|
+
* 4. Install system service
|
|
15
|
+
*/
|
|
16
|
+
export declare function install(port: number): InstallResult;
|
|
17
|
+
/**
|
|
18
|
+
* Full uninstallation:
|
|
19
|
+
* 1. Remove system service
|
|
20
|
+
* 2. Clear system proxy
|
|
21
|
+
* 3. Remove CA from trust store
|
|
22
|
+
*/
|
|
23
|
+
export declare function uninstall(purge?: boolean): InstallResult;
|
|
24
|
+
/** Check installation status */
|
|
25
|
+
export declare function status(port: number): Record<string, boolean | string>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.install = install;
|
|
37
|
+
exports.uninstall = uninstall;
|
|
38
|
+
exports.status = status;
|
|
39
|
+
const ca_manager_js_1 = require("../tls/ca-manager.js");
|
|
40
|
+
const trust_store_js_1 = require("../tls/trust-store.js");
|
|
41
|
+
const service_js_1 = require("../daemon/service.js");
|
|
42
|
+
const macos = __importStar(require("./macos.js"));
|
|
43
|
+
const linux = __importStar(require("./linux.js"));
|
|
44
|
+
const platform = process.platform === 'darwin' ? macos : linux;
|
|
45
|
+
/**
|
|
46
|
+
* Full installation:
|
|
47
|
+
* 1. Generate CA cert
|
|
48
|
+
* 2. Install CA in trust store
|
|
49
|
+
* 3. Configure system proxy
|
|
50
|
+
* 4. Install system service
|
|
51
|
+
*/
|
|
52
|
+
function install(port) {
|
|
53
|
+
const steps = [];
|
|
54
|
+
let allOk = true;
|
|
55
|
+
// Step 1: Generate CA
|
|
56
|
+
try {
|
|
57
|
+
(0, ca_manager_js_1.ensureCA)();
|
|
58
|
+
steps.push({ step: 'Generate CA certificate', success: true, message: `CA cert at ${(0, ca_manager_js_1.getCACertPath)()}` });
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
steps.push({ step: 'Generate CA certificate', success: false, message: String(err) });
|
|
62
|
+
allOk = false;
|
|
63
|
+
}
|
|
64
|
+
// Step 2: Install CA in trust store
|
|
65
|
+
if (allOk) {
|
|
66
|
+
const result = (0, trust_store_js_1.installCA)();
|
|
67
|
+
steps.push({ step: 'Install CA in trust store', success: result.success, message: result.message });
|
|
68
|
+
if (!result.success)
|
|
69
|
+
allOk = false;
|
|
70
|
+
}
|
|
71
|
+
// Step 3: Configure system proxy (PAC URL for selective proxying)
|
|
72
|
+
if (allOk) {
|
|
73
|
+
if (process.platform === 'darwin') {
|
|
74
|
+
const result = macos.setAutoproxyURL(`http://127.0.0.1:${port}/proxy.pac`);
|
|
75
|
+
steps.push({ step: 'Configure system proxy (PAC)', success: result.success, message: result.message });
|
|
76
|
+
if (!result.success)
|
|
77
|
+
allOk = false;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const result = platform.setSystemProxy('127.0.0.1', port);
|
|
81
|
+
steps.push({ step: 'Configure system proxy', success: result.success, message: result.message });
|
|
82
|
+
if (!result.success)
|
|
83
|
+
allOk = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Step 4: Install system service
|
|
87
|
+
if (allOk) {
|
|
88
|
+
try {
|
|
89
|
+
const servicePath = (0, service_js_1.installService)(port);
|
|
90
|
+
steps.push({ step: 'Install system service', success: true, message: `Service at ${servicePath}` });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
steps.push({ step: 'Install system service', success: false, message: String(err) });
|
|
94
|
+
// Non-fatal: proxy can still run manually
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Rollback on failure
|
|
98
|
+
if (!allOk) {
|
|
99
|
+
rollback(steps);
|
|
100
|
+
}
|
|
101
|
+
return { success: allOk, steps };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Full uninstallation:
|
|
105
|
+
* 1. Remove system service
|
|
106
|
+
* 2. Clear system proxy
|
|
107
|
+
* 3. Remove CA from trust store
|
|
108
|
+
*/
|
|
109
|
+
function uninstall(purge = false) {
|
|
110
|
+
const steps = [];
|
|
111
|
+
// Step 1: Remove service
|
|
112
|
+
try {
|
|
113
|
+
const msg = (0, service_js_1.uninstallService)();
|
|
114
|
+
steps.push({ step: 'Remove system service', success: true, message: msg });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
steps.push({ step: 'Remove system service', success: false, message: String(err) });
|
|
118
|
+
}
|
|
119
|
+
// Step 2: Clear proxy
|
|
120
|
+
if (process.platform === 'darwin') {
|
|
121
|
+
const r1 = macos.clearAutoproxyURL();
|
|
122
|
+
steps.push({ step: 'Clear auto-proxy', success: r1.success, message: r1.message });
|
|
123
|
+
const r2 = macos.clearSystemProxy();
|
|
124
|
+
steps.push({ step: 'Clear system proxy', success: r2.success, message: r2.message });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const r = linux.clearSystemProxy();
|
|
128
|
+
steps.push({ step: 'Clear system proxy', success: r.success, message: r.message });
|
|
129
|
+
}
|
|
130
|
+
// Step 3: Remove CA
|
|
131
|
+
const caResult = (0, trust_store_js_1.uninstallCA)();
|
|
132
|
+
steps.push({ step: 'Remove CA from trust store', success: caResult.success, message: caResult.message });
|
|
133
|
+
// Step 4: Purge data (optional)
|
|
134
|
+
if (purge) {
|
|
135
|
+
try {
|
|
136
|
+
const fs = require('node:fs');
|
|
137
|
+
const path = require('node:path');
|
|
138
|
+
const dataDir = path.join(process.env['HOME'] || '.', '.smartcontext');
|
|
139
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
140
|
+
steps.push({ step: 'Purge data directory', success: true, message: `Removed ${dataDir}` });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
steps.push({ step: 'Purge data directory', success: false, message: String(err) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const allOk = steps.every((s) => s.success);
|
|
147
|
+
return { success: allOk, steps };
|
|
148
|
+
}
|
|
149
|
+
/** Check installation status */
|
|
150
|
+
function status(port) {
|
|
151
|
+
return {
|
|
152
|
+
caExists: require('node:fs').existsSync((0, ca_manager_js_1.getCACertPath)()),
|
|
153
|
+
caInstalled: (0, trust_store_js_1.isCAInstalled)(),
|
|
154
|
+
proxyConfigured: platform.isProxyConfigured(port),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** Rollback completed steps on failure */
|
|
158
|
+
function rollback(completedSteps) {
|
|
159
|
+
for (const step of completedSteps.reverse()) {
|
|
160
|
+
if (!step.success)
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
if (step.step.includes('CA in trust store'))
|
|
164
|
+
(0, trust_store_js_1.uninstallCA)();
|
|
165
|
+
if (step.step.includes('system proxy') || step.step.includes('PAC')) {
|
|
166
|
+
if (process.platform === 'darwin') {
|
|
167
|
+
macos.clearAutoproxyURL();
|
|
168
|
+
macos.clearSystemProxy();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
linux.clearSystemProxy();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (step.step.includes('system service'))
|
|
175
|
+
(0, service_js_1.uninstallService)();
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=installer.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Configure Linux system proxy */
|
|
2
|
+
export declare function setSystemProxy(host: string, port: number): {
|
|
3
|
+
success: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
/** Clear Linux system proxy */
|
|
7
|
+
export declare function clearSystemProxy(): {
|
|
8
|
+
success: boolean;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function isProxyConfigured(port: number): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setSystemProxy = setSystemProxy;
|
|
4
|
+
exports.clearSystemProxy = clearSystemProxy;
|
|
5
|
+
exports.isProxyConfigured = isProxyConfigured;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
/** Configure Linux system proxy */
|
|
8
|
+
function setSystemProxy(host, port) {
|
|
9
|
+
const proxyUrl = `http://${host}:${port}`;
|
|
10
|
+
try {
|
|
11
|
+
// Try GNOME settings
|
|
12
|
+
try {
|
|
13
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy', 'mode', "'manual'"], { stdio: 'pipe' });
|
|
14
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy.http', 'host', `'${host}'`], { stdio: 'pipe' });
|
|
15
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy.http', 'port', String(port)], { stdio: 'pipe' });
|
|
16
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy.https', 'host', `'${host}'`], { stdio: 'pipe' });
|
|
17
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy.https', 'port', String(port)], { stdio: 'pipe' });
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
// Write environment file for shell sessions
|
|
21
|
+
const envFile = '/etc/profile.d/smartcontext-proxy.sh';
|
|
22
|
+
const content = `# SmartContext Proxy - auto-generated
|
|
23
|
+
export http_proxy="${proxyUrl}"
|
|
24
|
+
export https_proxy="${proxyUrl}"
|
|
25
|
+
export HTTP_PROXY="${proxyUrl}"
|
|
26
|
+
export HTTPS_PROXY="${proxyUrl}"
|
|
27
|
+
export no_proxy="localhost,127.0.0.1,::1"
|
|
28
|
+
`;
|
|
29
|
+
try {
|
|
30
|
+
(0, node_child_process_1.execFileSync)('sudo', ['tee', envFile], { input: content, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
return { success: true, message: `System proxy set to ${proxyUrl}` };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return { success: false, message: `Failed: ${err}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Clear Linux system proxy */
|
|
40
|
+
function clearSystemProxy() {
|
|
41
|
+
try {
|
|
42
|
+
try {
|
|
43
|
+
(0, node_child_process_1.execFileSync)('gsettings', ['set', 'org.gnome.system.proxy', 'mode', "'none'"], { stdio: 'pipe' });
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
try {
|
|
47
|
+
(0, node_child_process_1.execFileSync)('sudo', ['rm', '-f', '/etc/profile.d/smartcontext-proxy.sh'], { stdio: 'pipe' });
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
return { success: true, message: 'System proxy cleared' };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return { success: false, message: `Failed: ${err}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isProxyConfigured(port) {
|
|
57
|
+
const proxyEnv = process.env['https_proxy'] || process.env['HTTPS_PROXY'] || '';
|
|
58
|
+
return proxyEnv.includes(String(port));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=linux.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Get active network interface name (e.g., "Wi-Fi", "Ethernet") */
|
|
2
|
+
export declare function getActiveInterface(): string;
|
|
3
|
+
/** Configure macOS system proxy to use SmartContext */
|
|
4
|
+
export declare function setSystemProxy(host: string, port: number): {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
/** Remove system proxy configuration */
|
|
9
|
+
export declare function clearSystemProxy(): {
|
|
10
|
+
success: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
/** Set PAC URL as auto-proxy configuration */
|
|
14
|
+
export declare function setAutoproxyURL(pacUrl: string): {
|
|
15
|
+
success: boolean;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
/** Clear auto-proxy configuration */
|
|
19
|
+
export declare function clearAutoproxyURL(): {
|
|
20
|
+
success: boolean;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
/** Check if system proxy is currently set to SmartContext */
|
|
24
|
+
export declare function isProxyConfigured(port: number): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getActiveInterface = getActiveInterface;
|
|
4
|
+
exports.setSystemProxy = setSystemProxy;
|
|
5
|
+
exports.clearSystemProxy = clearSystemProxy;
|
|
6
|
+
exports.setAutoproxyURL = setAutoproxyURL;
|
|
7
|
+
exports.clearAutoproxyURL = clearAutoproxyURL;
|
|
8
|
+
exports.isProxyConfigured = isProxyConfigured;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
/** Get active network interface name (e.g., "Wi-Fi", "Ethernet") */
|
|
11
|
+
function getActiveInterface() {
|
|
12
|
+
try {
|
|
13
|
+
const route = (0, node_child_process_1.execFileSync)('route', ['-n', 'get', 'default'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
14
|
+
const ifMatch = route.match(/interface:\s*(\S+)/);
|
|
15
|
+
if (!ifMatch)
|
|
16
|
+
return 'Wi-Fi';
|
|
17
|
+
const ifName = ifMatch[1];
|
|
18
|
+
// Map interface ID to service name
|
|
19
|
+
const services = (0, node_child_process_1.execFileSync)('networksetup', ['-listallhardwareports'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
20
|
+
const lines = services.split('\n');
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
if (lines[i].includes(`Device: ${ifName}`)) {
|
|
23
|
+
const nameLine = lines[i - 1];
|
|
24
|
+
const nameMatch = nameLine?.match(/Hardware Port:\s*(.+)/);
|
|
25
|
+
if (nameMatch)
|
|
26
|
+
return nameMatch[1];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return 'Wi-Fi';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 'Wi-Fi';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Configure macOS system proxy to use SmartContext */
|
|
36
|
+
function setSystemProxy(host, port) {
|
|
37
|
+
const iface = getActiveInterface();
|
|
38
|
+
try {
|
|
39
|
+
// Set HTTPS proxy
|
|
40
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setsecurewebproxy', iface, host, String(port)], { stdio: 'pipe' });
|
|
41
|
+
// Set HTTP proxy
|
|
42
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setwebproxy', iface, host, String(port)], { stdio: 'pipe' });
|
|
43
|
+
// Enable them
|
|
44
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setsecurewebproxystate', iface, 'on'], { stdio: 'pipe' });
|
|
45
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setwebproxystate', iface, 'on'], { stdio: 'pipe' });
|
|
46
|
+
return { success: true, message: `System proxy set to ${host}:${port} on ${iface}` };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { success: false, message: `Failed to set proxy on ${iface}: ${err}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Remove system proxy configuration */
|
|
53
|
+
function clearSystemProxy() {
|
|
54
|
+
const iface = getActiveInterface();
|
|
55
|
+
try {
|
|
56
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setsecurewebproxystate', iface, 'off'], { stdio: 'pipe' });
|
|
57
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setwebproxystate', iface, 'off'], { stdio: 'pipe' });
|
|
58
|
+
return { success: true, message: `System proxy cleared on ${iface}` };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { success: false, message: `Failed to clear proxy on ${iface}: ${err}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Set PAC URL as auto-proxy configuration */
|
|
65
|
+
function setAutoproxyURL(pacUrl) {
|
|
66
|
+
const iface = getActiveInterface();
|
|
67
|
+
try {
|
|
68
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setautoproxyurl', iface, pacUrl], { stdio: 'pipe' });
|
|
69
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setautoproxystate', iface, 'on'], { stdio: 'pipe' });
|
|
70
|
+
return { success: true, message: `Auto-proxy URL set to ${pacUrl} on ${iface}` };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { success: false, message: `Failed: ${err}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Clear auto-proxy configuration */
|
|
77
|
+
function clearAutoproxyURL() {
|
|
78
|
+
const iface = getActiveInterface();
|
|
79
|
+
try {
|
|
80
|
+
(0, node_child_process_1.execFileSync)('networksetup', ['-setautoproxystate', iface, 'off'], { stdio: 'pipe' });
|
|
81
|
+
return { success: true, message: `Auto-proxy cleared on ${iface}` };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return { success: false, message: `Failed: ${err}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Check if system proxy is currently set to SmartContext */
|
|
88
|
+
function isProxyConfigured(port) {
|
|
89
|
+
const iface = getActiveInterface();
|
|
90
|
+
try {
|
|
91
|
+
const info = (0, node_child_process_1.execFileSync)('networksetup', ['-getsecurewebproxy', iface], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
|
+
return info.includes(`Port: ${port}`) && info.includes('Enabled: Yes');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=macos.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start watchdog that monitors proxy health.
|
|
3
|
+
* If proxy becomes unreachable, auto-removes system proxy config
|
|
4
|
+
* to prevent breaking the user's internet.
|
|
5
|
+
*/
|
|
6
|
+
export declare function startWatchdog(proxyPort: number): void;
|
|
7
|
+
export declare function stopWatchdog(): void;
|