@yhong91/cpac 0.1.25 → 0.1.27
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/README.md +17 -17
- package/dist/agents.js +445 -0
- package/dist/config.js +440 -0
- package/dist/cpac.js +105 -2441
- package/dist/proxy.js +341 -0
- package/dist/targets/claude.js +373 -0
- package/dist/targets/codex.js +378 -0
- package/dist/targets/grok.js +109 -0
- package/dist/targets/kimi.js +175 -0
- package/dist/targets/opencode.js +59 -0
- package/dist/targets/pi.js +32 -0
- package/dist/util.js +200 -0
- package/package.json +2 -3
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createServer, request as httpRequest, } from "node:http";
|
|
5
|
+
import { request as httpsRequest } from "node:https";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { apiBase, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
9
|
+
import { CPACError, atomicWrite, objectValue, resolveApiKey } from "./util.js";
|
|
10
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
11
|
+
"connection",
|
|
12
|
+
"keep-alive",
|
|
13
|
+
"proxy-authenticate",
|
|
14
|
+
"proxy-authorization",
|
|
15
|
+
"proxy-connection",
|
|
16
|
+
"te",
|
|
17
|
+
"trailer",
|
|
18
|
+
"transfer-encoding",
|
|
19
|
+
"upgrade",
|
|
20
|
+
]);
|
|
21
|
+
const CLIENT_CREDENTIAL_HEADERS = new Set([
|
|
22
|
+
"authorization",
|
|
23
|
+
"chatgpt-account-id",
|
|
24
|
+
"cookie",
|
|
25
|
+
"openai-organization",
|
|
26
|
+
"openai-project",
|
|
27
|
+
"x-api-key",
|
|
28
|
+
"x-opencodex-api-key",
|
|
29
|
+
]);
|
|
30
|
+
function localBrowserOrigin(value) {
|
|
31
|
+
if (!value)
|
|
32
|
+
return true;
|
|
33
|
+
try {
|
|
34
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
35
|
+
return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function proxyHeaders(headers, apiKey) {
|
|
42
|
+
const forwarded = {};
|
|
43
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
44
|
+
if (value === undefined ||
|
|
45
|
+
name === "host" ||
|
|
46
|
+
CLIENT_CREDENTIAL_HEADERS.has(name) ||
|
|
47
|
+
HOP_BY_HOP_HEADERS.has(name)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
forwarded[name] = value;
|
|
51
|
+
}
|
|
52
|
+
forwarded.authorization = `Bearer ${apiKey}`;
|
|
53
|
+
return forwarded;
|
|
54
|
+
}
|
|
55
|
+
export function responseHeaders(headers) {
|
|
56
|
+
const forwarded = {};
|
|
57
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
58
|
+
if (value === undefined || HOP_BY_HOP_HEADERS.has(name))
|
|
59
|
+
continue;
|
|
60
|
+
forwarded[name] = value;
|
|
61
|
+
}
|
|
62
|
+
return forwarded;
|
|
63
|
+
}
|
|
64
|
+
export function upstreamUrl(cpaUrl, requestUrl) {
|
|
65
|
+
try {
|
|
66
|
+
const incoming = new URL(requestUrl, "http://127.0.0.1");
|
|
67
|
+
const suffix = incoming.pathname === "/v1"
|
|
68
|
+
? ""
|
|
69
|
+
: incoming.pathname.startsWith("/v1/")
|
|
70
|
+
? incoming.pathname.slice(3)
|
|
71
|
+
: incoming.pathname;
|
|
72
|
+
return new URL(`${apiBase(cpaUrl)}${suffix}${incoming.search}`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new CPACError(`invalid upstream request URL: ${requestUrl}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
79
|
+
const server = createServer((request, response) => {
|
|
80
|
+
const requestUrl = request.url ?? "/";
|
|
81
|
+
if (requestUrl === "/_cpac/health") {
|
|
82
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
83
|
+
response.writeHead(404).end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
response.writeHead(204).end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (requestUrl === "/_cpac/shutdown" && request.method === "POST") {
|
|
90
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
91
|
+
response.writeHead(404).end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
response.writeHead(204).end();
|
|
95
|
+
setImmediate(() => {
|
|
96
|
+
server.closeAllConnections?.();
|
|
97
|
+
server.close();
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!localBrowserOrigin(request.headers.origin)) {
|
|
102
|
+
response.writeHead(403, { "content-type": "application/json" });
|
|
103
|
+
response.end(JSON.stringify({ error: "non-local origin rejected" }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
let target;
|
|
107
|
+
try {
|
|
108
|
+
target = upstreamUrl(cpaUrl, requestUrl);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
response.writeHead(400).end("invalid request URL");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
115
|
+
const upstream = send(target, {
|
|
116
|
+
method: request.method,
|
|
117
|
+
headers: proxyHeaders(request.headers, apiKey),
|
|
118
|
+
}, (upstreamResponse) => {
|
|
119
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
120
|
+
upstreamResponse.pipe(response);
|
|
121
|
+
});
|
|
122
|
+
upstream.once("error", () => {
|
|
123
|
+
if (!response.headersSent) {
|
|
124
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
125
|
+
}
|
|
126
|
+
if (!response.writableEnded)
|
|
127
|
+
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
128
|
+
});
|
|
129
|
+
request.once("aborted", () => upstream.destroy());
|
|
130
|
+
response.once("close", () => {
|
|
131
|
+
if (!response.writableEnded)
|
|
132
|
+
upstream.destroy();
|
|
133
|
+
});
|
|
134
|
+
request.pipe(upstream);
|
|
135
|
+
});
|
|
136
|
+
server.on("upgrade", (_request, socket) => {
|
|
137
|
+
socket.end("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
138
|
+
});
|
|
139
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
140
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
141
|
+
server.once("error", rejectListen);
|
|
142
|
+
server.listen(port, "127.0.0.1", () => {
|
|
143
|
+
server.off("error", rejectListen);
|
|
144
|
+
resolveListen();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
const address = server.address();
|
|
148
|
+
if (!address || typeof address === "string") {
|
|
149
|
+
server.close();
|
|
150
|
+
throw new CPACError("cannot determine loopback proxy port");
|
|
151
|
+
}
|
|
152
|
+
return { server, port: address.port };
|
|
153
|
+
}
|
|
154
|
+
export async function runProxyChild(port) {
|
|
155
|
+
const cpaUrl = process.env.CPAC_PROXY_UPSTREAM;
|
|
156
|
+
const apiKey = process.env.CPAC_PROXY_API_KEY;
|
|
157
|
+
const proxyId = process.env.CPAC_PROXY_ID;
|
|
158
|
+
if (!cpaUrl || !apiKey || !proxyId)
|
|
159
|
+
return 1;
|
|
160
|
+
try {
|
|
161
|
+
const proxy = await createLoopbackProxy(cpaUrl, apiKey, proxyId, port);
|
|
162
|
+
process.send?.({ ready: true, port: proxy.port, pid: process.pid });
|
|
163
|
+
const close = () => {
|
|
164
|
+
proxy.server.closeAllConnections?.();
|
|
165
|
+
proxy.server.close();
|
|
166
|
+
};
|
|
167
|
+
process.once("SIGINT", close);
|
|
168
|
+
process.once("SIGTERM", close);
|
|
169
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
process.send?.({
|
|
174
|
+
ready: false,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error),
|
|
176
|
+
});
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function proxyIsHealthy(proxy) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`http://127.0.0.1:${proxy.port}/_cpac/health`, {
|
|
183
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
184
|
+
signal: AbortSignal.timeout(1_000),
|
|
185
|
+
});
|
|
186
|
+
return response.status === 204;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function startProxyProcess(config, apiKey) {
|
|
193
|
+
const id = randomBytes(24).toString("hex");
|
|
194
|
+
const child = spawn(process.execPath, [
|
|
195
|
+
fileURLToPath(new URL("./cpac.js", import.meta.url)),
|
|
196
|
+
"_proxy",
|
|
197
|
+
String(config.codex_proxy_port),
|
|
198
|
+
], {
|
|
199
|
+
detached: true,
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
CPAC_PROXY_API_KEY: apiKey,
|
|
203
|
+
CPAC_PROXY_ID: id,
|
|
204
|
+
CPAC_PROXY_UPSTREAM: config.cpa_url,
|
|
205
|
+
},
|
|
206
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
207
|
+
});
|
|
208
|
+
return await new Promise((resolveStart, rejectStart) => {
|
|
209
|
+
let settled = false;
|
|
210
|
+
let timeout;
|
|
211
|
+
const finish = (error, value) => {
|
|
212
|
+
if (settled)
|
|
213
|
+
return;
|
|
214
|
+
settled = true;
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
child.removeAllListeners();
|
|
217
|
+
if (child.connected)
|
|
218
|
+
child.disconnect();
|
|
219
|
+
child.unref();
|
|
220
|
+
if (error)
|
|
221
|
+
rejectStart(error);
|
|
222
|
+
else
|
|
223
|
+
resolveStart(value);
|
|
224
|
+
};
|
|
225
|
+
timeout = setTimeout(() => {
|
|
226
|
+
child.kill("SIGKILL");
|
|
227
|
+
finish(new CPACError("loopback proxy did not become ready"));
|
|
228
|
+
}, 5_000);
|
|
229
|
+
child.once("error", (error) => finish(new CPACError(`cannot start loopback proxy: ${error.message}`)));
|
|
230
|
+
child.once("exit", (code) => finish(new CPACError(`loopback proxy exited before ready (${code ?? 1})`)));
|
|
231
|
+
child.once("message", (message) => {
|
|
232
|
+
if (objectValue(message) &&
|
|
233
|
+
message.ready === true &&
|
|
234
|
+
typeof message.pid === "number" &&
|
|
235
|
+
typeof message.port === "number") {
|
|
236
|
+
finish(undefined, { id, pid: message.pid, port: message.port });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const detail = objectValue(message) && typeof message.error === "string"
|
|
240
|
+
? `: ${message.error}`
|
|
241
|
+
: "";
|
|
242
|
+
finish(new CPACError(`cannot start loopback proxy${detail}`));
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function processCommandArgs(pid) {
|
|
247
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
248
|
+
return undefined;
|
|
249
|
+
try {
|
|
250
|
+
const proc = `/proc/${pid}/cmdline`;
|
|
251
|
+
if (existsSync(proc)) {
|
|
252
|
+
const raw = readFileSync(proc, "utf8");
|
|
253
|
+
return raw ? raw.split("\0").filter(Boolean) : undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const result = spawnSync("ps", ["-p", String(pid), "-www", "-o", "args="], {
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
timeout: 1_000,
|
|
263
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
264
|
+
});
|
|
265
|
+
if (result.status !== 0)
|
|
266
|
+
return undefined;
|
|
267
|
+
const line = result.stdout.trim();
|
|
268
|
+
return line ? line.split(/\s+/) : undefined;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function isCpacProxyProcess(pid) {
|
|
275
|
+
const args = processCommandArgs(pid);
|
|
276
|
+
if (!args)
|
|
277
|
+
return false;
|
|
278
|
+
return (args.includes("_proxy") &&
|
|
279
|
+
args.some((arg) => arg === "cpac.js" || arg.endsWith("/cpac.js") || arg.endsWith("\\cpac.js")));
|
|
280
|
+
}
|
|
281
|
+
function killCpacProxy(pid, signal) {
|
|
282
|
+
if (!isCpacProxyProcess(pid))
|
|
283
|
+
return false;
|
|
284
|
+
try {
|
|
285
|
+
process.kill(pid, signal);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export async function stopProxyProcess(proxy) {
|
|
293
|
+
if (await proxyIsHealthy(proxy)) {
|
|
294
|
+
try {
|
|
295
|
+
await fetch(`http://127.0.0.1:${proxy.port}/_cpac/shutdown`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
298
|
+
signal: AbortSignal.timeout(1_000),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// The server may close the connection while shutting down.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!killCpacProxy(proxy.pid, "SIGTERM"))
|
|
306
|
+
return;
|
|
307
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 200));
|
|
308
|
+
killCpacProxy(proxy.pid, "SIGKILL");
|
|
309
|
+
}
|
|
310
|
+
export async function runProxy(config) {
|
|
311
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
312
|
+
const state = readState(config.state_dir);
|
|
313
|
+
if (!state)
|
|
314
|
+
throw new CPACError("no active CPAC injection; run cpac inject first");
|
|
315
|
+
originalBytes(config.state_dir, state, config.codex_config);
|
|
316
|
+
const recorded = stateProxy(state);
|
|
317
|
+
if (!recorded) {
|
|
318
|
+
throw new CPACError("injection has no loopback proxy state; run cpac inject to migrate it");
|
|
319
|
+
}
|
|
320
|
+
if (await proxyIsHealthy(recorded)) {
|
|
321
|
+
throw new CPACError(`loopback proxy is already running on 127.0.0.1:${recorded.port}`);
|
|
322
|
+
}
|
|
323
|
+
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
324
|
+
try {
|
|
325
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey)));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
proxy.server.closeAllConnections?.();
|
|
329
|
+
proxy.server.close();
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
console.log(`CPAC loopback proxy listening on http://127.0.0.1:${proxy.port}/v1`);
|
|
333
|
+
const close = () => {
|
|
334
|
+
proxy.server.closeAllConnections?.();
|
|
335
|
+
proxy.server.close();
|
|
336
|
+
};
|
|
337
|
+
process.once("SIGINT", close);
|
|
338
|
+
process.once("SIGTERM", close);
|
|
339
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
340
|
+
return 0;
|
|
341
|
+
}
|