@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 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.34.1",
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": {