@askalf/dario 3.34.1 → 3.35.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/dist/cli.js +45 -0
- package/dist/doctor.js +26 -0
- package/dist/outbound-proxy.d.ts +35 -0
- package/dist/outbound-proxy.js +142 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import { startProxy, sanitizeError } from './proxy.js';
|
|
|
26
26
|
import { VALID_EFFORT_VALUES } from './cc-template.js';
|
|
27
27
|
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
28
28
|
import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
|
|
29
|
+
import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-proxy.js';
|
|
29
30
|
// `args` / `command` at module scope — command handlers below close over
|
|
30
31
|
// `args` to read their own flags. Reading argv is harmless on import; only
|
|
31
32
|
// the handler dispatch at the bottom is gated behind the main-entry check.
|
|
@@ -298,6 +299,36 @@ async function proxy() {
|
|
|
298
299
|
// startup. A bad path fails fast rather than silently degrading to
|
|
299
300
|
// verbatim — same fail-loud philosophy as --strict-tls / --strict-template.
|
|
300
301
|
const systemPrompt = resolveSystemPromptFlag(args, process.env['DARIO_SYSTEM_PROMPT']);
|
|
302
|
+
// --upstream-proxy=URL / --via=URL (v3.35.0) — route all of dario's
|
|
303
|
+
// outbound fetch() calls through an HTTP/HTTPS proxy. Pair with the
|
|
304
|
+
// HTTP-proxy mode of a VPN provider (Mullvad, AirVPN), a corporate
|
|
305
|
+
// proxy, privoxy/Tor, etc. Localhost calls bypass.
|
|
306
|
+
//
|
|
307
|
+
// Requires Bun runtime — Node's built-in fetch ignores the proxy
|
|
308
|
+
// option silently. SOCKS5 not supported (rejected at parse time).
|
|
309
|
+
// See docs/vpn-routing.md for the full setup options.
|
|
310
|
+
const outboundProxyArg = args.find((a) => a.startsWith('--upstream-proxy=')) ?? args.find((a) => a.startsWith('--via='));
|
|
311
|
+
const outboundProxyRaw = outboundProxyArg
|
|
312
|
+
? outboundProxyArg.split('=').slice(1).join('=')
|
|
313
|
+
: process.env['DARIO_UPSTREAM_PROXY'];
|
|
314
|
+
let outboundProxy = null;
|
|
315
|
+
try {
|
|
316
|
+
outboundProxy = parseOutboundProxy(outboundProxyRaw);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
console.error(`[dario] ${err.message}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
if (outboundProxy) {
|
|
323
|
+
try {
|
|
324
|
+
installOutboundProxyWrapper(outboundProxy);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.error(`[dario] ${err.message}`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
console.error(`[dario] Outbound proxy: ${outboundProxy.display} (all upstream fetches routed; localhost bypasses)`);
|
|
331
|
+
}
|
|
301
332
|
// --passthrough-betas=name1,name2 — operator-pinned beta allow-list.
|
|
302
333
|
// Names listed here are always forwarded to Anthropic regardless of
|
|
303
334
|
// CC's captured set or the client's own beta header; bypasses the
|
|
@@ -968,6 +999,20 @@ async function help() {
|
|
|
968
999
|
on your account but isn't in the captured
|
|
969
1000
|
template. Env: DARIO_PASSTHROUGH_BETAS.
|
|
970
1001
|
|
|
1002
|
+
--upstream-proxy=URL / --via=URL
|
|
1003
|
+
Route all of dario's outbound fetch
|
|
1004
|
+
calls (api.anthropic.com, OpenAI-compat
|
|
1005
|
+
backends, OAuth) through an HTTP/HTTPS
|
|
1006
|
+
proxy. Localhost calls bypass. Useful
|
|
1007
|
+
with a VPN provider's HTTP proxy mode
|
|
1008
|
+
(Mullvad, AirVPN, corporate proxy,
|
|
1009
|
+
privoxy/Tor) when you don't want to put
|
|
1010
|
+
the whole system on a system VPN.
|
|
1011
|
+
Requires Bun runtime. SOCKS5 not
|
|
1012
|
+
supported (Bun fetch limitation). See
|
|
1013
|
+
docs/vpn-routing.md. Env: DARIO_UPSTREAM_PROXY.
|
|
1014
|
+
(v3.35.0)
|
|
1015
|
+
|
|
971
1016
|
--system-prompt=<MODE> System-prompt mode for outbound CC-shaped
|
|
972
1017
|
requests (v3.34.0). One of:
|
|
973
1018
|
verbatim — CC unchanged (default)
|
package/dist/doctor.js
CHANGED
|
@@ -268,6 +268,32 @@ export async function runChecks(opts = {}) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
catch { /* never let prompt-mode reporting break the doctor */ }
|
|
271
|
+
// ---- Outbound proxy mode (v3.35.0)
|
|
272
|
+
// Surfaces whether `--upstream-proxy` / DARIO_UPSTREAM_PROXY is set.
|
|
273
|
+
// Doctor runs without a live proxy, so we read the env-var path only
|
|
274
|
+
// (the CLI flag's effect is in-process and not visible from doctor).
|
|
275
|
+
// Credentials in the URL are masked; only host:port is shown.
|
|
276
|
+
try {
|
|
277
|
+
const rawProxy = process.env['DARIO_UPSTREAM_PROXY'];
|
|
278
|
+
if (rawProxy && rawProxy.trim() !== '') {
|
|
279
|
+
let display = rawProxy;
|
|
280
|
+
try {
|
|
281
|
+
const u = new URL(rawProxy);
|
|
282
|
+
if (u.username)
|
|
283
|
+
u.username = '***';
|
|
284
|
+
if (u.password)
|
|
285
|
+
u.password = '***';
|
|
286
|
+
display = u.toString();
|
|
287
|
+
}
|
|
288
|
+
catch { /* leave raw if unparseable; CLI will error at startup */ }
|
|
289
|
+
checks.push({
|
|
290
|
+
status: 'info',
|
|
291
|
+
label: 'Outbound proxy',
|
|
292
|
+
detail: `DARIO_UPSTREAM_PROXY=${display}. Upstream fetches routed via this proxy; localhost calls bypass. Requires Bun runtime. See docs/vpn-routing.md.`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch { /* never let proxy reporting break the doctor */ }
|
|
271
297
|
// ---- Template drift
|
|
272
298
|
try {
|
|
273
299
|
const drift = detectDrift(CC_TEMPLATE);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface OutboundProxyConfig {
|
|
2
|
+
/** Original URL string supplied by the user. Passed verbatim to fetch's `proxy` option. */
|
|
3
|
+
url: string;
|
|
4
|
+
/** Parsed scheme — http or https. SOCKS rejected at parse time. */
|
|
5
|
+
scheme: 'http' | 'https';
|
|
6
|
+
/** Sanitized URL for logging — credentials redacted. */
|
|
7
|
+
display: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse and validate an outbound-proxy URL. Returns null for empty/undefined
|
|
11
|
+
* input (no proxy configured). Throws with a clear message on:
|
|
12
|
+
* - URL parse failure
|
|
13
|
+
* - SOCKS scheme (unsupported by Bun fetch)
|
|
14
|
+
* - Other unsupported schemes
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseOutboundProxy(raw: string | undefined): OutboundProxyConfig | null;
|
|
17
|
+
/**
|
|
18
|
+
* Heuristic check: does this URL target localhost / loopback?
|
|
19
|
+
* Used to skip the proxy wrapper for self-targeting fetches (doctor
|
|
20
|
+
* pings the local server, etc.). Lenient on parse errors — anything
|
|
21
|
+
* unparseable returns false (proxied as a bare hostname, conservatively).
|
|
22
|
+
*/
|
|
23
|
+
export declare function isLocalhostUrl(input: unknown): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Install a global fetch wrapper that adds `{ proxy }` to outbound
|
|
26
|
+
* (non-localhost) calls. Idempotent over a single dario startup —
|
|
27
|
+
* called once from cli.ts before startProxy.
|
|
28
|
+
*
|
|
29
|
+
* Refuses to install on non-Bun runtimes because Node's built-in fetch
|
|
30
|
+
* silently ignores the proxy option, which would yield false-success
|
|
31
|
+
* behavior (requests appearing to route through the proxy when they
|
|
32
|
+
* actually go direct). Better to fail loud at startup than fail silent
|
|
33
|
+
* at request time.
|
|
34
|
+
*/
|
|
35
|
+
export declare function installOutboundProxyWrapper(config: OutboundProxyConfig): void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Optional outbound-proxy routing for upstream API calls. Behind
|
|
2
|
+
// `--upstream-proxy=URL` / `--via=URL` / `DARIO_UPSTREAM_PROXY`, dario
|
|
3
|
+
// routes all of its outbound fetch() calls — `api.anthropic.com`,
|
|
4
|
+
// configured OpenAI-compat backends, OAuth flows, drift checks, doctor
|
|
5
|
+
// probes — through the supplied proxy. Localhost-bound fetches bypass
|
|
6
|
+
// it (the inbound HTTP server is unaffected; this only wraps egress).
|
|
7
|
+
//
|
|
8
|
+
// Use case: security-conscious users who want dario's upstream traffic
|
|
9
|
+
// routed through their VPN provider's HTTP proxy endpoint without
|
|
10
|
+
// putting the entire host on a system-level VPN. Pair with the HTTP
|
|
11
|
+
// proxy mode of Mullvad / AirVPN / a local privoxy-on-Tor / corporate
|
|
12
|
+
// proxy infrastructure / Cloudflare WARP via gateway / etc.
|
|
13
|
+
//
|
|
14
|
+
// Runtime constraints:
|
|
15
|
+
// - Requires Bun. Bun's fetch implements the `proxy` option natively;
|
|
16
|
+
// Node's built-in fetch (undici-backed) ignores it silently and
|
|
17
|
+
// would yield a misleading "looks like it's working" failure mode.
|
|
18
|
+
// dario already auto-relaunches under Bun when available; if the
|
|
19
|
+
// user is on Node and sets this flag, refuse to start with a clear
|
|
20
|
+
// "install Bun" message.
|
|
21
|
+
// - HTTP/HTTPS proxies only. SOCKS5 is not supported by Bun 1.3.x's
|
|
22
|
+
// fetch (`UnsupportedProxyProtocol`). Most VPN providers expose an
|
|
23
|
+
// HTTP proxy endpoint alongside their SOCKS5 one (Mullvad, AirVPN);
|
|
24
|
+
// point the flag at that.
|
|
25
|
+
//
|
|
26
|
+
// Wire-fidelity note: the proxy sits *outside* the TLS session — TLS
|
|
27
|
+
// to api.anthropic.com terminates at Anthropic, not at the proxy.
|
|
28
|
+
// Bun's BoringSSL ClientHello is preserved end-to-end. The only thing
|
|
29
|
+
// the proxy can see in HTTPS-CONNECT mode is the destination hostname
|
|
30
|
+
// (via SNI) and the byte timing.
|
|
31
|
+
/**
|
|
32
|
+
* Parse and validate an outbound-proxy URL. Returns null for empty/undefined
|
|
33
|
+
* input (no proxy configured). Throws with a clear message on:
|
|
34
|
+
* - URL parse failure
|
|
35
|
+
* - SOCKS scheme (unsupported by Bun fetch)
|
|
36
|
+
* - Other unsupported schemes
|
|
37
|
+
*/
|
|
38
|
+
export function parseOutboundProxy(raw) {
|
|
39
|
+
if (!raw || raw.trim() === '')
|
|
40
|
+
return null;
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = new URL(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`--upstream-proxy: ${JSON.stringify(raw)} is not a valid URL. Expected http://host:port or https://host:port.`);
|
|
47
|
+
}
|
|
48
|
+
const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
|
|
49
|
+
if (scheme === 'socks5' || scheme === 'socks5h' || scheme === 'socks4' || scheme === 'socks4a' || scheme === 'socks') {
|
|
50
|
+
throw new Error(`--upstream-proxy: SOCKS5 is not supported by the underlying fetch runtime (Bun 1.3.x). ` +
|
|
51
|
+
`Use the HTTP proxy endpoint of your VPN provider instead — e.g. Mullvad / AirVPN / corporate proxy / privoxy-on-Tor all expose http://host:port. ` +
|
|
52
|
+
`If your provider only exposes SOCKS5, run a local SOCKS-to-HTTP bridge (privoxy with forward-socks5) and point dario at the HTTP side.`);
|
|
53
|
+
}
|
|
54
|
+
if (scheme !== 'http' && scheme !== 'https') {
|
|
55
|
+
throw new Error(`--upstream-proxy: unsupported scheme ${JSON.stringify(scheme)}. Use http:// or https://.`);
|
|
56
|
+
}
|
|
57
|
+
// Sanitize for logging: hide username/password if embedded in URL.
|
|
58
|
+
const display = (() => {
|
|
59
|
+
if (!parsed.username && !parsed.password)
|
|
60
|
+
return parsed.toString();
|
|
61
|
+
const safe = new URL(parsed.toString());
|
|
62
|
+
if (safe.username)
|
|
63
|
+
safe.username = '***';
|
|
64
|
+
if (safe.password)
|
|
65
|
+
safe.password = '***';
|
|
66
|
+
return safe.toString();
|
|
67
|
+
})();
|
|
68
|
+
return { url: parsed.toString(), scheme: scheme, display };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Heuristic check: does this URL target localhost / loopback?
|
|
72
|
+
* Used to skip the proxy wrapper for self-targeting fetches (doctor
|
|
73
|
+
* pings the local server, etc.). Lenient on parse errors — anything
|
|
74
|
+
* unparseable returns false (proxied as a bare hostname, conservatively).
|
|
75
|
+
*/
|
|
76
|
+
export function isLocalhostUrl(input) {
|
|
77
|
+
if (input === null || input === undefined)
|
|
78
|
+
return false;
|
|
79
|
+
let urlStr;
|
|
80
|
+
if (typeof input === 'string') {
|
|
81
|
+
urlStr = input;
|
|
82
|
+
}
|
|
83
|
+
else if (input instanceof URL) {
|
|
84
|
+
urlStr = input.toString();
|
|
85
|
+
}
|
|
86
|
+
else if (typeof input === 'object' && 'url' in input) {
|
|
87
|
+
const u = input.url;
|
|
88
|
+
urlStr = typeof u === 'string' ? u : '';
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!urlStr)
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = new URL(urlStr);
|
|
97
|
+
// URL.hostname for IPv6 includes the brackets ([::1]); strip for matching.
|
|
98
|
+
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
99
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '::1')
|
|
100
|
+
return true;
|
|
101
|
+
if (host.endsWith('.localhost'))
|
|
102
|
+
return true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Install a global fetch wrapper that adds `{ proxy }` to outbound
|
|
111
|
+
* (non-localhost) calls. Idempotent over a single dario startup —
|
|
112
|
+
* called once from cli.ts before startProxy.
|
|
113
|
+
*
|
|
114
|
+
* Refuses to install on non-Bun runtimes because Node's built-in fetch
|
|
115
|
+
* silently ignores the proxy option, which would yield false-success
|
|
116
|
+
* behavior (requests appearing to route through the proxy when they
|
|
117
|
+
* actually go direct). Better to fail loud at startup than fail silent
|
|
118
|
+
* at request time.
|
|
119
|
+
*/
|
|
120
|
+
export function installOutboundProxyWrapper(config) {
|
|
121
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
122
|
+
if (!isBun) {
|
|
123
|
+
throw new Error(`--upstream-proxy requires the Bun runtime. Node's built-in fetch ignores the \`proxy\` option silently — ` +
|
|
124
|
+
`the flag would appear to work while requests actually went direct. Install Bun (https://bun.sh) and re-run; ` +
|
|
125
|
+
`dario auto-relaunches under Bun when available, or you can run \`bun run\` directly.`);
|
|
126
|
+
}
|
|
127
|
+
const originalFetch = globalThis.fetch;
|
|
128
|
+
// Wrap. Localhost targets bypass the proxy (loopback shouldn't tunnel).
|
|
129
|
+
// The wrapper preserves originalFetch's behavior for everything else
|
|
130
|
+
// and adds the `proxy` field. Bun honors it; the rest of the args
|
|
131
|
+
// (headers, body, signal, dispatcher, etc.) pass through unchanged.
|
|
132
|
+
const wrapped = ((input, init) => {
|
|
133
|
+
if (isLocalhostUrl(input)) {
|
|
134
|
+
return originalFetch(input, init);
|
|
135
|
+
}
|
|
136
|
+
// Use a typed cast — Bun's fetch options include `proxy`, but TS's
|
|
137
|
+
// standard fetch types don't.
|
|
138
|
+
const bunInit = { ...(init || {}), proxy: config.url };
|
|
139
|
+
return originalFetch(input, bunInit);
|
|
140
|
+
});
|
|
141
|
+
globalThis.fetch = wrapped;
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|