ai-cc-router 0.4.0 → 0.4.2
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/dist/cli/cmd-client.js
CHANGED
|
@@ -122,7 +122,7 @@ export function registerClient(program) {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
if (wantsDesktop) {
|
|
125
|
-
await setupDesktopInterception(url);
|
|
125
|
+
await setupDesktopInterception(url, secret);
|
|
126
126
|
cfg.client.desktopEnabled = true;
|
|
127
127
|
writeConfig(cfg);
|
|
128
128
|
}
|
|
@@ -277,7 +277,7 @@ export function registerClient(program) {
|
|
|
277
277
|
process.exit(1);
|
|
278
278
|
}
|
|
279
279
|
if (!cfg.client.desktopEnabled) {
|
|
280
|
-
await setupDesktopInterception(cfg.client.remoteUrl);
|
|
280
|
+
await setupDesktopInterception(cfg.client.remoteUrl, cfg.client.remoteSecret);
|
|
281
281
|
cfg.client.desktopEnabled = true;
|
|
282
282
|
writeConfig(cfg);
|
|
283
283
|
}
|
|
@@ -305,11 +305,12 @@ export function registerClient(program) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
const target = cfg.client.remoteUrl;
|
|
308
|
+
const secret = cfg.client.remoteSecret;
|
|
308
309
|
const processName = getProcessName();
|
|
309
310
|
console.log(chalk.cyan(`\nStarting mitmproxy interceptor for "${processName}"...`));
|
|
310
311
|
console.log(chalk.gray(` Redirecting api.anthropic.com/v1/messages → ${target}`));
|
|
311
312
|
try {
|
|
312
|
-
await startInterceptor(target);
|
|
313
|
+
await startInterceptor(target, secret);
|
|
313
314
|
}
|
|
314
315
|
catch (e) {
|
|
315
316
|
console.error(chalk.red(`\n✗ Failed to start interceptor:\n`));
|
|
@@ -325,7 +326,7 @@ export function registerClient(program) {
|
|
|
325
326
|
default: true,
|
|
326
327
|
});
|
|
327
328
|
if (autoStart) {
|
|
328
|
-
const ok = await installInterceptorService(target);
|
|
329
|
+
const ok = await installInterceptorService(target, secret);
|
|
329
330
|
if (ok) {
|
|
330
331
|
cfg.client.desktopAutoStart = true;
|
|
331
332
|
writeConfig(cfg);
|
|
@@ -408,7 +409,7 @@ export function printNetworkExtensionInstructions() {
|
|
|
408
409
|
console.log(" " + chalk.cyan("5.") + " Enter your Mac admin password when prompted\n");
|
|
409
410
|
console.log(chalk.gray(" You only need to do this ONCE per machine.\n"));
|
|
410
411
|
}
|
|
411
|
-
async function setupDesktopInterception(target) {
|
|
412
|
+
async function setupDesktopInterception(target, secret) {
|
|
412
413
|
console.log(chalk.bold("\n🖥 Claude Desktop Setup\n"));
|
|
413
414
|
// 0. Explain what actually works before anything else
|
|
414
415
|
printDesktopSupportExplainer();
|
|
@@ -474,8 +475,8 @@ async function setupDesktopInterception(target) {
|
|
|
474
475
|
console.log(chalk.gray(" ~/.mitmproxy/mitmproxy-ca-cert.pem"));
|
|
475
476
|
}
|
|
476
477
|
}
|
|
477
|
-
// 4. Write addon script
|
|
478
|
-
writeAddonScript(target);
|
|
478
|
+
// 4. Write addon script (with secret so intercepted requests authenticate)
|
|
479
|
+
writeAddonScript(target, secret);
|
|
479
480
|
console.log(chalk.green("✓ Redirect addon configured"));
|
|
480
481
|
// 5. macOS Network Extension — THIS is the step people miss
|
|
481
482
|
if (isMacos()) {
|
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -400,7 +400,7 @@ async function runClientSetupFromWizard() {
|
|
|
400
400
|
default: false,
|
|
401
401
|
});
|
|
402
402
|
if (wantsDesktop) {
|
|
403
|
-
await setupDesktopFromWizard(url);
|
|
403
|
+
await setupDesktopFromWizard(url, secret);
|
|
404
404
|
cfg.client.desktopEnabled = true;
|
|
405
405
|
writeConfig(cfg);
|
|
406
406
|
}
|
|
@@ -413,7 +413,7 @@ async function runClientSetupFromWizard() {
|
|
|
413
413
|
}
|
|
414
414
|
console.log();
|
|
415
415
|
}
|
|
416
|
-
async function setupDesktopFromWizard(target) {
|
|
416
|
+
async function setupDesktopFromWizard(target, secret) {
|
|
417
417
|
console.log(chalk.bold("\n🖥 Claude Desktop — Cowork / Agent Setup\n"));
|
|
418
418
|
// 1. Check mitmproxy
|
|
419
419
|
if (!(await checkMitmproxyInstalled())) {
|
|
@@ -459,8 +459,8 @@ async function setupDesktopFromWizard(target) {
|
|
|
459
459
|
console.log(chalk.gray(" -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem"));
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
|
-
// 3. Addon
|
|
463
|
-
writeAddonScript(target);
|
|
462
|
+
// 3. Addon (with secret so intercepted requests authenticate against the proxy)
|
|
463
|
+
writeAddonScript(target, secret);
|
|
464
464
|
console.log(chalk.green("✓ Redirect addon configured"));
|
|
465
465
|
// 4. Network Extension walkthrough (macOS)
|
|
466
466
|
if (isMacos()) {
|
|
@@ -192,12 +192,13 @@ export async function installCaCert() {
|
|
|
192
192
|
* Write the redirect addon to ~/.cc-router/interceptor/addon.py.
|
|
193
193
|
* Uses the bundled template as source.
|
|
194
194
|
*/
|
|
195
|
-
export function writeAddonScript(target) {
|
|
195
|
+
export function writeAddonScript(target, secret) {
|
|
196
196
|
if (!existsSync(ADDON_DIR))
|
|
197
197
|
mkdirSync(ADDON_DIR, { recursive: true });
|
|
198
198
|
// Try to use the bundled addon as template; fall back to a minimal inline version.
|
|
199
|
-
// In BOTH cases we inject the actual target URL so the addon is
|
|
200
|
-
// and doesn't depend on the CC_ROUTER_TARGET
|
|
199
|
+
// In BOTH cases we inject the actual target URL (and secret) so the addon is
|
|
200
|
+
// self-contained and doesn't depend on the CC_ROUTER_TARGET / CC_ROUTER_SECRET
|
|
201
|
+
// env vars being present at runtime.
|
|
201
202
|
const bundled = addonSourcePath();
|
|
202
203
|
let src;
|
|
203
204
|
if (existsSync(bundled)) {
|
|
@@ -217,6 +218,8 @@ _target_parsed = urlparse(_target)
|
|
|
217
218
|
if not _target_parsed.scheme or not _target_parsed.netloc:
|
|
218
219
|
raise RuntimeError(f"CC_ROUTER_TARGET is not a valid URL: {_target_raw!r}")
|
|
219
220
|
|
|
221
|
+
_secret = os.environ.get("CC_ROUTER_SECRET", "")
|
|
222
|
+
|
|
220
223
|
_REDIRECT_PREFIXES = ("/v1/messages", "/v1/models")
|
|
221
224
|
|
|
222
225
|
def request(flow: http.HTTPFlow) -> None:
|
|
@@ -228,11 +231,17 @@ def request(flow: http.HTTPFlow) -> None:
|
|
|
228
231
|
flow.request.host = _target_parsed.hostname or "localhost"
|
|
229
232
|
flow.request.port = _target_parsed.port or (443 if _target_parsed.scheme == "https" else 80)
|
|
230
233
|
flow.request.headers["host"] = flow.request.host + (f":{flow.request.port}" if flow.request.port not in (80, 443) else "")
|
|
234
|
+
if _secret:
|
|
235
|
+
flow.request.headers["x-api-key"] = _secret
|
|
231
236
|
`.trimStart();
|
|
232
237
|
}
|
|
233
|
-
// Inject the
|
|
234
|
-
// without the CC_ROUTER_TARGET
|
|
238
|
+
// Inject the target URL and (optionally) secret into the default values so
|
|
239
|
+
// the addon works even without the CC_ROUTER_TARGET / CC_ROUTER_SECRET env
|
|
240
|
+
// vars being present at runtime (manual mitmdump restarts, launchd, etc.).
|
|
235
241
|
src = src.replace('"http://localhost:3456"', JSON.stringify(target));
|
|
242
|
+
if (secret) {
|
|
243
|
+
src = src.replace('os.environ.get("CC_ROUTER_SECRET", "")', `os.environ.get("CC_ROUTER_SECRET", ${JSON.stringify(secret)})`);
|
|
244
|
+
}
|
|
236
245
|
writeFileSync(ADDON_PATH, src, "utf-8");
|
|
237
246
|
}
|
|
238
247
|
// ─── Interceptor lifecycle ────────────────────────────────────────────────────
|
|
@@ -240,7 +249,7 @@ def request(flow: http.HTTPFlow) -> None:
|
|
|
240
249
|
* Start mitmdump in local mode, intercepting the Claude process and redirecting
|
|
241
250
|
* api.anthropic.com traffic to CC-Router via the addon script.
|
|
242
251
|
*/
|
|
243
|
-
export async function startInterceptor(target) {
|
|
252
|
+
export async function startInterceptor(target, secret) {
|
|
244
253
|
// On macOS, verify the Network Extension is enabled before attempting to start.
|
|
245
254
|
// If it's "waiting", mitmdump starts silently but captures zero traffic.
|
|
246
255
|
if (isMacos()) {
|
|
@@ -259,9 +268,9 @@ export async function startInterceptor(target) {
|
|
|
259
268
|
" Then re-run this command.");
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
// Always (re)write the addon script so package updates and target-URL
|
|
272
|
+
// changes are picked up automatically without requiring a fresh setup.
|
|
273
|
+
writeAddonScript(target, secret);
|
|
265
274
|
const processName = getProcessName();
|
|
266
275
|
const args = [
|
|
267
276
|
"--mode", `local:${processName}`,
|
|
@@ -269,10 +278,13 @@ export async function startInterceptor(target) {
|
|
|
269
278
|
"--set", "connection_strategy=lazy",
|
|
270
279
|
"--quiet",
|
|
271
280
|
];
|
|
281
|
+
const env = { ...process.env, CC_ROUTER_TARGET: target };
|
|
282
|
+
if (secret)
|
|
283
|
+
env["CC_ROUTER_SECRET"] = secret;
|
|
272
284
|
const child = spawn("mitmdump", args, {
|
|
273
285
|
detached: true,
|
|
274
286
|
stdio: "ignore",
|
|
275
|
-
env
|
|
287
|
+
env,
|
|
276
288
|
});
|
|
277
289
|
child.unref();
|
|
278
290
|
if (child.pid) {
|
|
@@ -337,8 +349,12 @@ async function resolveMitmdumpPath() {
|
|
|
337
349
|
return "mitmdump"; // fallback — hope it's on PATH at boot time
|
|
338
350
|
}
|
|
339
351
|
}
|
|
340
|
-
function buildInterceptorPlist(mitmdumpPath, target) {
|
|
352
|
+
function buildInterceptorPlist(mitmdumpPath, target, secret) {
|
|
341
353
|
const processName = getProcessName();
|
|
354
|
+
const secretEntry = secret
|
|
355
|
+
? ` <key>CC_ROUTER_SECRET</key>
|
|
356
|
+
<string>${secret}</string>`
|
|
357
|
+
: "";
|
|
342
358
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
343
359
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
344
360
|
<plist version="1.0">
|
|
@@ -375,13 +391,15 @@ function buildInterceptorPlist(mitmdumpPath, target) {
|
|
|
375
391
|
<string>${process.env["PATH"] ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"}</string>
|
|
376
392
|
<key>CC_ROUTER_TARGET</key>
|
|
377
393
|
<string>${target}</string>
|
|
394
|
+
${secretEntry}
|
|
378
395
|
</dict>
|
|
379
396
|
</dict>
|
|
380
397
|
</plist>
|
|
381
398
|
`;
|
|
382
399
|
}
|
|
383
|
-
function buildInterceptorSystemdUnit(mitmdumpPath, target) {
|
|
400
|
+
function buildInterceptorSystemdUnit(mitmdumpPath, target, secret) {
|
|
384
401
|
const processName = getProcessName();
|
|
402
|
+
const secretLine = secret ? `\nEnvironment=CC_ROUTER_SECRET=${secret}` : "";
|
|
385
403
|
return `[Unit]
|
|
386
404
|
Description=CC-Router Interceptor — mitmproxy for Claude Desktop
|
|
387
405
|
After=network-online.target
|
|
@@ -395,7 +413,7 @@ RestartSec=5
|
|
|
395
413
|
StartLimitIntervalSec=60
|
|
396
414
|
StartLimitBurst=5
|
|
397
415
|
Environment=PATH=${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}
|
|
398
|
-
Environment=CC_ROUTER_TARGET=${target}
|
|
416
|
+
Environment=CC_ROUTER_TARGET=${target}${secretLine}
|
|
399
417
|
|
|
400
418
|
[Install]
|
|
401
419
|
WantedBy=default.target
|
|
@@ -405,17 +423,17 @@ WantedBy=default.target
|
|
|
405
423
|
* Install the mitmproxy interceptor as an OS service so it starts on boot.
|
|
406
424
|
* Stops any existing detached mitmdump process first — the OS service takes over.
|
|
407
425
|
*/
|
|
408
|
-
export async function installInterceptorService(target) {
|
|
409
|
-
// Ensure the addon script is up-to-date with the target URL
|
|
410
|
-
writeAddonScript(target);
|
|
426
|
+
export async function installInterceptorService(target, secret) {
|
|
427
|
+
// Ensure the addon script is up-to-date with the target URL and secret
|
|
428
|
+
writeAddonScript(target, secret);
|
|
411
429
|
// Stop any manually-spawned mitmdump — OS service will manage it now
|
|
412
430
|
await stopInterceptor();
|
|
413
431
|
const mitmdumpPath = await resolveMitmdumpPath();
|
|
414
432
|
const platform = detectPlatform();
|
|
415
433
|
switch (platform) {
|
|
416
|
-
case "macos": return installInterceptorMacOS(mitmdumpPath, target);
|
|
417
|
-
case "linux": return installInterceptorLinux(mitmdumpPath, target);
|
|
418
|
-
case "windows": return installInterceptorWindows(mitmdumpPath, target);
|
|
434
|
+
case "macos": return installInterceptorMacOS(mitmdumpPath, target, secret);
|
|
435
|
+
case "linux": return installInterceptorLinux(mitmdumpPath, target, secret);
|
|
436
|
+
case "windows": return installInterceptorWindows(mitmdumpPath, target, secret);
|
|
419
437
|
}
|
|
420
438
|
}
|
|
421
439
|
export async function uninstallInterceptorService() {
|
|
@@ -435,7 +453,7 @@ export function isInterceptorServiceInstalled() {
|
|
|
435
453
|
}
|
|
436
454
|
}
|
|
437
455
|
// ─── macOS LaunchAgent ──────────────────────────────────────────────────────
|
|
438
|
-
async function installInterceptorMacOS(mitmdumpPath, target) {
|
|
456
|
+
async function installInterceptorMacOS(mitmdumpPath, target, secret) {
|
|
439
457
|
const launchAgentsDir = dirname(LAUNCHD_PLIST);
|
|
440
458
|
if (!existsSync(launchAgentsDir))
|
|
441
459
|
mkdirSync(launchAgentsDir, { recursive: true });
|
|
@@ -443,7 +461,7 @@ async function installInterceptorMacOS(mitmdumpPath, target) {
|
|
|
443
461
|
if (existsSync(LAUNCHD_PLIST)) {
|
|
444
462
|
await interceptorLaunchctlUnload();
|
|
445
463
|
}
|
|
446
|
-
writeFileSync(LAUNCHD_PLIST, buildInterceptorPlist(mitmdumpPath, target), "utf-8");
|
|
464
|
+
writeFileSync(LAUNCHD_PLIST, buildInterceptorPlist(mitmdumpPath, target, secret), "utf-8");
|
|
447
465
|
// Load — try modern `bootstrap` first, fallback to legacy `load`
|
|
448
466
|
const uid = String(process.getuid?.() ?? 501);
|
|
449
467
|
try {
|
|
@@ -483,10 +501,10 @@ async function interceptorLaunchctlUnload() {
|
|
|
483
501
|
}
|
|
484
502
|
}
|
|
485
503
|
// ─── Linux systemd user service ─────────────────────────────────────────────
|
|
486
|
-
async function installInterceptorLinux(mitmdumpPath, target) {
|
|
504
|
+
async function installInterceptorLinux(mitmdumpPath, target, secret) {
|
|
487
505
|
if (!existsSync(SYSTEMD_DIR))
|
|
488
506
|
mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
489
|
-
writeFileSync(SYSTEMD_SERVICE, buildInterceptorSystemdUnit(mitmdumpPath, target), "utf-8");
|
|
507
|
+
writeFileSync(SYSTEMD_SERVICE, buildInterceptorSystemdUnit(mitmdumpPath, target, secret), "utf-8");
|
|
490
508
|
try {
|
|
491
509
|
await execFileP("systemctl", ["--user", "daemon-reload"]);
|
|
492
510
|
await execFileP("systemctl", ["--user", "enable", "cc-router-interceptor"]);
|
|
@@ -517,9 +535,10 @@ async function uninstallInterceptorLinux() {
|
|
|
517
535
|
catch { /* ok */ }
|
|
518
536
|
}
|
|
519
537
|
// ─── Windows Registry ───────────────────────────────────────────────────────
|
|
520
|
-
async function installInterceptorWindows(mitmdumpPath, target) {
|
|
538
|
+
async function installInterceptorWindows(mitmdumpPath, target, secret) {
|
|
521
539
|
const processName = getProcessName();
|
|
522
|
-
const
|
|
540
|
+
const secretEnv = secret ? `set CC_ROUTER_SECRET=${secret} && ` : "";
|
|
541
|
+
const cmd = `cmd /c "set CC_ROUTER_TARGET=${target} && ${secretEnv}"${mitmdumpPath}" --mode local:${processName} -s "${ADDON_PATH}" --set connection_strategy=lazy --quiet"`;
|
|
523
542
|
try {
|
|
524
543
|
await execFileP("reg", [
|
|
525
544
|
"add", WINDOWS_REG_KEY,
|
package/package.json
CHANGED
package/src/interceptor/addon.py
CHANGED
|
@@ -24,6 +24,9 @@ _target_parsed = urlparse(_target)
|
|
|
24
24
|
if not _target_parsed.scheme or not _target_parsed.netloc:
|
|
25
25
|
raise RuntimeError(f"CC_ROUTER_TARGET is not a valid URL: {_target_raw!r}")
|
|
26
26
|
|
|
27
|
+
# Optional proxy secret — when set, injected as x-api-key on redirected requests
|
|
28
|
+
_secret = os.environ.get("CC_ROUTER_SECRET", "")
|
|
29
|
+
|
|
27
30
|
# Paths that CC-Router can handle (it injects its own OAuth token)
|
|
28
31
|
_REDIRECT_PREFIXES = (
|
|
29
32
|
"/v1/messages",
|
|
@@ -47,3 +50,7 @@ def request(flow: http.HTTPFlow) -> None:
|
|
|
47
50
|
if flow.request.port not in (80, 443)
|
|
48
51
|
else ""
|
|
49
52
|
)
|
|
53
|
+
|
|
54
|
+
# Authenticate against the proxy if a secret is configured
|
|
55
|
+
if _secret:
|
|
56
|
+
flow.request.headers["x-api-key"] = _secret
|