@askalf/dario 3.22.0 → 3.23.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/README.md +13 -12
- package/dist/cli.js +14 -1
- package/dist/doctor.js +20 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +25 -0
- package/dist/runtime-fingerprint.d.ts +77 -0
- package/dist/runtime-fingerprint.js +117 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ One command, one local URL, every provider behind it. Point `ANTHROPIC_BASE_URL`
|
|
|
26
26
|
- `llama-3.3-70b`, `deepseek-v3`, anything else → **Groq**, **OpenRouter**, **local LiteLLM**, **vLLM**, **Ollama**, whichever OpenAI-compat backend you wired up
|
|
27
27
|
- Force a backend explicitly with a prefix: `openai:gpt-4o`, `groq:llama-3.3-70b`, `local:qwen-coder`, `claude:opus`
|
|
28
28
|
|
|
29
|
-
Switching providers is a **model-name change** in your tool. Not a reconfigure. Not new base URLs. Not new API keys. Not a new SDK import. **Zero runtime dependencies. ~
|
|
29
|
+
Switching providers is a **model-name change** in your tool. Not a reconfigure. Not new base URLs. Not new API keys. Not a new SDK import. **Zero runtime dependencies. ~8,100 lines of TypeScript across ~15 files. ~840 assertions across 24 test suites. [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) on every release. Nothing phones home, ever.**
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -88,7 +88,7 @@ Something broken? `dario doctor` prints a single aggregated health report — da
|
|
|
88
88
|
|
|
89
89
|
**You hit rate limits on long agent runs.** Add a second / third Claude subscription with `dario accounts add work` and pool mode routes each request to whichever account has the most headroom. **Session stickiness** (v3.13.0) pins a multi-turn conversation to one account so the Anthropic prompt cache survives the run. **In-flight 429 failover** retries the same request against a different account before your client sees an error. See [Multi-account pool mode](#multi-account-pool-mode).
|
|
90
90
|
|
|
91
|
-
**You run a coding agent that isn't Claude Code.** Cline, Roo Code, Cursor, Windsurf, Continue.dev, GitHub Copilot, OpenHands, OpenClaw, Hermes — they each ship their own tool schemas and their own validators. Dario's universal `TOOL_MAP` (
|
|
91
|
+
**You run a coding agent that isn't Claude Code.** Cline, Roo Code, Cursor, Windsurf, Continue.dev, GitHub Copilot, OpenHands, OpenClaw, Hermes — they each ship their own tool schemas and their own validators. Dario's universal `TOOL_MAP` (**~66 schema-verified entries**) pre-maps every major coding agent's tool names to Claude Code's native set on the outbound path and rebuilds to your agent's exact expected shape on the inbound path. No `--preserve-tools`, no fingerprint loss, no validator errors. See [Agent compatibility](#agent-compatibility).
|
|
92
92
|
|
|
93
93
|
**You want the proxy layer off the wire entirely.** **Shim mode** (v3.12, hardened in v3.13) is an in-process `globalThis.fetch` patch injected via `NODE_OPTIONS=--require`. No HTTP hop, no port to bind, no `BASE_URL` to set. `dario shim -- claude --print "hi"` and CC thinks it's talking directly to `api.anthropic.com`. See [Shim mode](#shim-mode).
|
|
94
94
|
|
|
@@ -156,11 +156,11 @@ Force a backend with a **provider prefix** on the model field (`openai:gpt-4o`,
|
|
|
156
156
|
|
|
157
157
|
OAuth-backed Claude Max / Pro, billed against your plan instead of the API. Activated by `dario login`.
|
|
158
158
|
|
|
159
|
-
**What it does.** Every outbound Claude request is rebuilt to look exactly like a request Claude Code itself would make — system prompt, tool definitions, fingerprint headers, billing tag, beta flags, **even the exact header insertion order** — using a live-extracted template from your actually-installed CC binary that self-heals on every Anthropic release. Anthropic's classifier sees a CC session because, from the wire up, it *is* one. That's what keeps your usage on subscription billing instead of API overage.
|
|
159
|
+
**What it does.** Every outbound Claude request is rebuilt to look exactly like a request Claude Code itself would make — system prompt, tool definitions, fingerprint headers, billing tag, beta flags, **even the exact header insertion order and request-body key order** — using a live-extracted template from your actually-installed CC binary that self-heals on every Anthropic release. Anthropic's classifier sees a CC session because, from the wire up, it *is* one. That's what keeps your usage on subscription billing instead of API overage.
|
|
160
160
|
|
|
161
161
|
**Key mechanisms:**
|
|
162
162
|
|
|
163
|
-
- **Live fingerprint extraction** (v3.11). Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template (system prompt, tools, user-agent, beta flags, **header insertion order** as of v3.13
|
|
163
|
+
- **Live fingerprint extraction** (v3.11). Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template (system prompt, tools, user-agent, beta flags, **header insertion order** as of v3.13 replayed by the shim since v3.13 and the proxy since v3.16, **static header values** + **`anthropic-beta` flags** as of v3.19, and **top-level request-body key order** as of v3.22). Eliminates the "Anthropic ships a new CC, dario is stale for 48 hours" window. Cached at `~/.dario/cc-template.live.json` with a 24h TTL. Falls back to the bundled snapshot if CC isn't installed; the bundled snapshot is scrubbed of host-identifying paths at bake time (v3.21).
|
|
164
164
|
- **Drift detection** (v3.17). On startup dario probes the installed `claude` binary and compares against the captured template. Mismatch triggers a forced refresh and prints a one-line warning. Users never silently sit on a stale template again.
|
|
165
165
|
- **Compat matrix** (v3.17). `SUPPORTED_CC_RANGE = { min: "1.0.0", maxTested: "2.1.104" }` is encoded in code. Installed CC outside that band prints a warn (untested above) or fail (below min) — zero-dep dotted-numeric comparator, no `semver` import per the dep policy.
|
|
166
166
|
- **Billing tag** reconstructed using CC's own algorithm: `x-anthropic-billing-header: cc_version=<version>.<build_tag>; cc_entrypoint=cli; cch=<5-char-hex>;` where `build_tag = SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)`.
|
|
@@ -272,7 +272,7 @@ Under the hood: `dario shim` spawns the child with `NODE_OPTIONS=--require <dari
|
|
|
272
272
|
|
|
273
273
|
## Agent compatibility
|
|
274
274
|
|
|
275
|
-
As of **v3.
|
|
275
|
+
As of **v3.22**, dario's built-in `TOOL_MAP` carries **~66 schema-verified entries** covering the tool schemas of every major coding agent. On the Claude backend, tool calls translate to CC's native `Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch` on the outbound path (keeping the subscription fingerprint intact) and rebuild to your agent's exact expected shape on the inbound path (so your validator is happy). No flag required.
|
|
276
276
|
|
|
277
277
|
| Agent | Covered tool names (subset) |
|
|
278
278
|
|---|---|
|
|
@@ -515,11 +515,11 @@ Dario handles your OAuth tokens and API keys locally. Here's why you can trust i
|
|
|
515
515
|
|
|
516
516
|
| Signal | Status |
|
|
517
517
|
|---|---|
|
|
518
|
-
| **Source code** | ~
|
|
518
|
+
| **Source code** | ~8,100 lines of TypeScript across ~15 files — small enough to audit in a weekend |
|
|
519
519
|
| **Dependencies** | 0 runtime dependencies. Verify: `npm ls --production` |
|
|
520
520
|
| **npm provenance** | Every release is [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions with sigstore provenance attached to the transparency log |
|
|
521
521
|
| **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
|
|
522
|
-
| **Test footprint** | ~
|
|
522
|
+
| **Test footprint** | ~840 assertions across 24 files. Full `npm test` green on every release |
|
|
523
523
|
| **Credential handling** | Tokens and API keys never logged, redacted from errors, stored with `0600` permissions |
|
|
524
524
|
| **OAuth flow** | PKCE (Proof Key for Code Exchange), no client secret |
|
|
525
525
|
| **Network scope** | Binds to `127.0.0.1` by default. `--host` allows LAN/mesh with `DARIO_API_KEY` gating. Upstream traffic goes only to the configured backend target URLs over HTTPS |
|
|
@@ -568,7 +568,7 @@ Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter,
|
|
|
568
568
|
Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v4.json`, keyed by the CC binary fingerprint.
|
|
569
569
|
|
|
570
570
|
**What happens when Anthropic changes the CC request template?**
|
|
571
|
-
Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags,
|
|
571
|
+
Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection (v3.17) forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
|
|
572
572
|
|
|
573
573
|
**First time setup on a fresh Claude account.**
|
|
574
574
|
If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
|
|
@@ -617,15 +617,16 @@ Longer-form writing on how dario works and why it works that way:
|
|
|
617
617
|
|
|
618
618
|
## Contributing
|
|
619
619
|
|
|
620
|
-
PRs welcome. The codebase is small TypeScript — ~
|
|
620
|
+
PRs welcome. The codebase is small TypeScript — ~8,100 lines across ~15 files:
|
|
621
621
|
|
|
622
622
|
| File | Purpose |
|
|
623
623
|
|---|---|
|
|
624
624
|
| `src/proxy.ts` | HTTP proxy server, request handler, rate governor, Claude backend dispatch, OpenAI-compat routing, pool failover |
|
|
625
|
-
| `src/cc-template.ts` | CC request template engine, universal `TOOL_MAP` (~
|
|
626
|
-
| `src/cc-template-data.json` | Bundled fallback CC request template (used when live-fingerprint extraction isn't possible) |
|
|
625
|
+
| `src/cc-template.ts` | CC request template engine, universal `TOOL_MAP` (~66 schema-verified entries), orchestration and framework scrubbing, header-order + body-field-order replay |
|
|
626
|
+
| `src/cc-template-data.json` | Bundled fallback CC request template (used when live-fingerprint extraction isn't possible). Scrubbed of host-identifying paths at bake time. |
|
|
627
|
+
| `src/scrub-template.ts` | Host-context scrubber for the baked fallback template — strips per-session sections, replaces user-dir paths with a placeholder, drops `mcp__*` tools (v3.21) |
|
|
627
628
|
| `src/cc-oauth-detect.ts` | OAuth config auto-detection from the installed CC binary |
|
|
628
|
-
| `src/live-fingerprint.ts` | Live extraction of the CC request template (system prompt, tools, user-agent, beta flags, header order) from the installed Claude Code binary, drift detection, compat matrix, atomic cache writes, corruption recovery |
|
|
629
|
+
| `src/live-fingerprint.ts` | Live extraction of the CC request template (system prompt, tools, user-agent, beta flags, header order, static header values, body field order) from the installed Claude Code binary, drift detection, compat matrix, atomic cache writes, corruption recovery |
|
|
629
630
|
| `src/doctor.ts` | `dario doctor` health report aggregator — dario/Node/CC/template/drift/OAuth/pool/backends |
|
|
630
631
|
| `src/oauth.ts` | Single-account token storage, PKCE flow, auto-refresh |
|
|
631
632
|
| `src/accounts.ts` | Multi-account credential storage, independent OAuth lifecycle, refresh single-flight |
|
package/dist/cli.js
CHANGED
|
@@ -200,9 +200,15 @@ async function proxy() {
|
|
|
200
200
|
// when Cline/Kilo/Roo is detected can pass --no-auto-detect; they keep
|
|
201
201
|
// explicit control with --preserve-tools per session. dario#40 (ringge).
|
|
202
202
|
const noAutoDetect = args.includes('--no-auto-detect') || args.includes('--no-auto-preserve');
|
|
203
|
+
// --strict-tls refuses to start proxy mode when the process's TLS stack
|
|
204
|
+
// doesn't match Claude Code's (i.e. we're on Node without Bun). Opt-in
|
|
205
|
+
// hard guardrail for operators who want certainty that the JA3 the
|
|
206
|
+
// proxy presents to Anthropic is Bun's BoringSSL ClientHello, not
|
|
207
|
+
// Node's OpenSSL one. v3.23 (direction #3).
|
|
208
|
+
const strictTls = args.includes('--strict-tls');
|
|
203
209
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
204
210
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
205
|
-
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, noAutoDetect });
|
|
211
|
+
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, noAutoDetect, strictTls });
|
|
206
212
|
}
|
|
207
213
|
async function accounts() {
|
|
208
214
|
const sub = args[1];
|
|
@@ -446,6 +452,13 @@ async function help() {
|
|
|
446
452
|
intact even when a text-tool client is
|
|
447
453
|
detected; use --preserve-tools per session
|
|
448
454
|
when edits are needed. (dario#40)
|
|
455
|
+
--strict-tls Refuse to start proxy mode if this process
|
|
456
|
+
isn't running under Bun. Bun is what Claude
|
|
457
|
+
Code uses; matching its TLS stack keeps the
|
|
458
|
+
proxy's JA3/JA4 ClientHello indistinguishable
|
|
459
|
+
from a stock CC request. Install Bun
|
|
460
|
+
(https://bun.sh) so dario auto-relaunches
|
|
461
|
+
under it, or use shim mode. (v3.23)
|
|
449
462
|
--port=PORT Port to listen on (default: 3456)
|
|
450
463
|
--host=ADDRESS Address to bind to (default: 127.0.0.1)
|
|
451
464
|
Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
|
package/dist/doctor.js
CHANGED
|
@@ -72,6 +72,26 @@ export async function runChecks() {
|
|
|
72
72
|
label: 'Platform',
|
|
73
73
|
detail: `${platform()} ${arch()} (${release()})`,
|
|
74
74
|
});
|
|
75
|
+
// ---- Runtime TLS fingerprint (v3.23, direction #3)
|
|
76
|
+
// Proxy mode terminates TLS in this process, so Bun-vs-Node is a
|
|
77
|
+
// fingerprint axis Anthropic can read directly off the wire.
|
|
78
|
+
try {
|
|
79
|
+
const { detectRuntimeFingerprint } = await import('./runtime-fingerprint.js');
|
|
80
|
+
const rt = detectRuntimeFingerprint();
|
|
81
|
+
const status = rt.status === 'bun-match' ? 'ok' : 'warn';
|
|
82
|
+
checks.push({
|
|
83
|
+
status,
|
|
84
|
+
label: 'Runtime / TLS',
|
|
85
|
+
detail: rt.hint ? `${rt.detail}. ${rt.hint}` : rt.detail,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
checks.push({
|
|
90
|
+
status: 'warn',
|
|
91
|
+
label: 'Runtime / TLS',
|
|
92
|
+
detail: `check failed: ${err.message}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
75
95
|
// ---- CC binary
|
|
76
96
|
const cc = safely(() => findInstalledCC(), { path: null, version: null });
|
|
77
97
|
if (cc.path && cc.version) {
|
package/dist/proxy.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface ProxyOptions {
|
|
|
12
12
|
preserveTools?: boolean;
|
|
13
13
|
hybridTools?: boolean;
|
|
14
14
|
noAutoDetect?: boolean;
|
|
15
|
+
strictTls?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export declare function sanitizeError(err: unknown): string;
|
|
17
18
|
export declare function startProxy(opts?: ProxyOptions): Promise<void>;
|
package/dist/proxy.js
CHANGED
|
@@ -363,6 +363,22 @@ export async function startProxy(opts = {}) {
|
|
|
363
363
|
const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
|
|
364
364
|
const verbose = opts.verbose ?? false;
|
|
365
365
|
const passthrough = opts.passthrough ?? false;
|
|
366
|
+
// TLS-fingerprint axis (v3.23, direction #3). Proxy mode terminates TLS
|
|
367
|
+
// to api.anthropic.com from this process; if we're not on Bun, the
|
|
368
|
+
// ClientHello that reaches Anthropic is Node's OpenSSL shape, not CC's
|
|
369
|
+
// Bun/BoringSSL shape. `--strict-tls` turns this silent divergence into
|
|
370
|
+
// a startup refusal. Doctor + the always-on banner below surface the
|
|
371
|
+
// same information without aborting, for users who know they're fine
|
|
372
|
+
// (API-key billing, single-call invocations, shim-mode-elsewhere, etc.).
|
|
373
|
+
const { detectRuntimeFingerprint } = await import('./runtime-fingerprint.js');
|
|
374
|
+
const runtimeFp = detectRuntimeFingerprint();
|
|
375
|
+
if (opts.strictTls && runtimeFp.status !== 'bun-match') {
|
|
376
|
+
console.error(`[dario] --strict-tls: ${runtimeFp.detail}`);
|
|
377
|
+
if (runtimeFp.hint)
|
|
378
|
+
console.error(`[dario] → ${runtimeFp.hint}`);
|
|
379
|
+
console.error('[dario] refusing to start proxy mode. Omit --strict-tls to run anyway.');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
366
382
|
// Text-tool-protocol client families that have already logged a
|
|
367
383
|
// "detected → auto-enabling preserve-tools" banner this session.
|
|
368
384
|
// Set once on first sighting per family so the startup log stays
|
|
@@ -1635,6 +1651,15 @@ export async function startProxy(opts = {}) {
|
|
|
1635
1651
|
if (compat.status === 'below-min' || compat.status === 'untested-above') {
|
|
1636
1652
|
console.log(`[dario] ⚠ CC compat: ${compat.message}`);
|
|
1637
1653
|
}
|
|
1654
|
+
// TLS-fingerprint banner (v3.23). Proxy mode terminates TLS from this
|
|
1655
|
+
// process, so the Bun-vs-Node runtime choice is actually on the wire.
|
|
1656
|
+
// Silence via DARIO_QUIET_TLS=1 for known-fine environments.
|
|
1657
|
+
if (runtimeFp.status !== 'bun-match' && process.env.DARIO_QUIET_TLS !== '1') {
|
|
1658
|
+
console.log(`[dario] ⚠ TLS fingerprint: ${runtimeFp.detail}`);
|
|
1659
|
+
if (runtimeFp.hint)
|
|
1660
|
+
console.log(`[dario] → ${runtimeFp.hint}`);
|
|
1661
|
+
console.log('[dario] (silence with DARIO_QUIET_TLS=1, or use --strict-tls to hard-fail)');
|
|
1662
|
+
}
|
|
1638
1663
|
// Kick off a live fingerprint refresh in the background. Re-captures the
|
|
1639
1664
|
// user's own CC binary request shape and updates ~/.dario/cc-template.live.json
|
|
1640
1665
|
// for the next startup. No-op if CC isn't installed or the cache is fresh.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime TLS-fingerprint detector (direction #3 from the v3.22 roadmap).
|
|
3
|
+
*
|
|
4
|
+
* The Claude Code binary is a Bun-compiled standalone executable, so every
|
|
5
|
+
* HTTPS request it makes goes out through Bun's BoringSSL-derived TLS stack.
|
|
6
|
+
* That ClientHello (JA3/JA4 hash) is what Anthropic's TLS-layer classifier
|
|
7
|
+
* actually sees on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Dario has two transports with different exposure to this axis:
|
|
10
|
+
*
|
|
11
|
+
* - **Shim mode** runs inside CC's own process (NODE_OPTIONS=--require),
|
|
12
|
+
* so its outbound fetch rides on CC's TLS stack by construction.
|
|
13
|
+
* Nothing to reconcile — the shim is always TLS-matched to CC.
|
|
14
|
+
*
|
|
15
|
+
* - **Proxy mode** is a separate process holding its own TLS sessions
|
|
16
|
+
* to api.anthropic.com. Anthropic sees the proxy's TLS fingerprint,
|
|
17
|
+
* not the consumer client's. If the proxy runs under Node, the
|
|
18
|
+
* ClientHello is OpenSSL-shaped — distinct from Bun's BoringSSL shape.
|
|
19
|
+
* That's the JA3 gap this module flags.
|
|
20
|
+
*
|
|
21
|
+
* Mitigation today: dario auto-relaunches under Bun when Bun is on PATH
|
|
22
|
+
* (see top of `src/cli.ts`). When Bun isn't available the auto-relaunch
|
|
23
|
+
* is a silent no-op, so proxy mode silently runs on Node's TLS stack
|
|
24
|
+
* with no indication to the operator. This module makes the runtime
|
|
25
|
+
* status a first-class check: `dario doctor` reports it, proxy startup
|
|
26
|
+
* warns when the axis is mismatched, and `--strict-tls` hard-fails
|
|
27
|
+
* instead of silently running with a divergent fingerprint.
|
|
28
|
+
*
|
|
29
|
+
* Pure-function: every input is passed in explicitly so tests can
|
|
30
|
+
* exercise each runtime combination without spawning processes.
|
|
31
|
+
*/
|
|
32
|
+
/** Canonical buckets the caller pivots on. */
|
|
33
|
+
export type RuntimeFingerprintStatus =
|
|
34
|
+
/** Running under Bun — TLS stack matches CC. */
|
|
35
|
+
'bun-match'
|
|
36
|
+
/** Running under Node, Bun available on PATH but auto-relaunch was bypassed. */
|
|
37
|
+
| 'bun-bypassed'
|
|
38
|
+
/** Running under Node, Bun not installed. */
|
|
39
|
+
| 'node-only';
|
|
40
|
+
export interface RuntimeFingerprint {
|
|
41
|
+
status: RuntimeFingerprintStatus;
|
|
42
|
+
/** 'bun' or 'node' — which runtime this process is actually on. */
|
|
43
|
+
runtime: 'bun' | 'node';
|
|
44
|
+
/** Version string from the runtime (e.g. "1.1.30" or "v20.11.1"). */
|
|
45
|
+
runtimeVersion: string;
|
|
46
|
+
/** Bun version discovered on PATH, if any. undefined when runtime==='bun' or bun-not-found. */
|
|
47
|
+
availableBunVersion?: string;
|
|
48
|
+
/** Why auto-relaunch didn't fire when `status === 'bun-bypassed'`. */
|
|
49
|
+
bypassReason?: 'DARIO_NO_BUN' | 'unknown';
|
|
50
|
+
/** Human-readable one-line explanation for the check label. */
|
|
51
|
+
detail: string;
|
|
52
|
+
/** Actionable hint when status !== 'bun-match'. undefined otherwise. */
|
|
53
|
+
hint?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Probe the Bun binary on PATH without relaunching. Returns undefined
|
|
57
|
+
* when bun isn't installed or the version probe fails for any reason
|
|
58
|
+
* (timeout, non-zero exit, etc.). Kept synchronous to match cli.ts's
|
|
59
|
+
* pre-import flow; doctor.ts is the only other caller and is fine with
|
|
60
|
+
* the (~sub-100ms) cost when Bun is installed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function probeBunVersion(): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Synthesize the TLS-fingerprint status from three inputs. All three are
|
|
65
|
+
* passed explicitly so tests can cover every combination without touching
|
|
66
|
+
* the real environment. Production callers pass
|
|
67
|
+
* `classifyRuntimeFingerprint(typeof Bun !== 'undefined', probeBunVersion(), process.env)`.
|
|
68
|
+
*
|
|
69
|
+
* The `env` parameter is read-only — this function never mutates it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function classifyRuntimeFingerprint(runningUnderBun: boolean, availableBunVersion: string | undefined, env: Record<string, string | undefined>, nodeVersion?: string): RuntimeFingerprint;
|
|
72
|
+
/**
|
|
73
|
+
* Convenience wrapper that reads the current process state. doctor.ts
|
|
74
|
+
* calls this once; tests do not — they exercise classifyRuntimeFingerprint
|
|
75
|
+
* directly with synthetic inputs.
|
|
76
|
+
*/
|
|
77
|
+
export declare function detectRuntimeFingerprint(): RuntimeFingerprint;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime TLS-fingerprint detector (direction #3 from the v3.22 roadmap).
|
|
3
|
+
*
|
|
4
|
+
* The Claude Code binary is a Bun-compiled standalone executable, so every
|
|
5
|
+
* HTTPS request it makes goes out through Bun's BoringSSL-derived TLS stack.
|
|
6
|
+
* That ClientHello (JA3/JA4 hash) is what Anthropic's TLS-layer classifier
|
|
7
|
+
* actually sees on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Dario has two transports with different exposure to this axis:
|
|
10
|
+
*
|
|
11
|
+
* - **Shim mode** runs inside CC's own process (NODE_OPTIONS=--require),
|
|
12
|
+
* so its outbound fetch rides on CC's TLS stack by construction.
|
|
13
|
+
* Nothing to reconcile — the shim is always TLS-matched to CC.
|
|
14
|
+
*
|
|
15
|
+
* - **Proxy mode** is a separate process holding its own TLS sessions
|
|
16
|
+
* to api.anthropic.com. Anthropic sees the proxy's TLS fingerprint,
|
|
17
|
+
* not the consumer client's. If the proxy runs under Node, the
|
|
18
|
+
* ClientHello is OpenSSL-shaped — distinct from Bun's BoringSSL shape.
|
|
19
|
+
* That's the JA3 gap this module flags.
|
|
20
|
+
*
|
|
21
|
+
* Mitigation today: dario auto-relaunches under Bun when Bun is on PATH
|
|
22
|
+
* (see top of `src/cli.ts`). When Bun isn't available the auto-relaunch
|
|
23
|
+
* is a silent no-op, so proxy mode silently runs on Node's TLS stack
|
|
24
|
+
* with no indication to the operator. This module makes the runtime
|
|
25
|
+
* status a first-class check: `dario doctor` reports it, proxy startup
|
|
26
|
+
* warns when the axis is mismatched, and `--strict-tls` hard-fails
|
|
27
|
+
* instead of silently running with a divergent fingerprint.
|
|
28
|
+
*
|
|
29
|
+
* Pure-function: every input is passed in explicitly so tests can
|
|
30
|
+
* exercise each runtime combination without spawning processes.
|
|
31
|
+
*/
|
|
32
|
+
import { execFileSync } from 'node:child_process';
|
|
33
|
+
/**
|
|
34
|
+
* Probe the Bun binary on PATH without relaunching. Returns undefined
|
|
35
|
+
* when bun isn't installed or the version probe fails for any reason
|
|
36
|
+
* (timeout, non-zero exit, etc.). Kept synchronous to match cli.ts's
|
|
37
|
+
* pre-import flow; doctor.ts is the only other caller and is fine with
|
|
38
|
+
* the (~sub-100ms) cost when Bun is installed.
|
|
39
|
+
*/
|
|
40
|
+
export function probeBunVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync('bun', ['--version'], {
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
+
timeout: 3000,
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
});
|
|
47
|
+
const trimmed = out.trim();
|
|
48
|
+
// `bun --version` prints just the version like "1.1.30". Reject anything
|
|
49
|
+
// longer than a sanity threshold so an unrelated `bun` binary can't
|
|
50
|
+
// poison the detection.
|
|
51
|
+
if (trimmed.length > 0 && trimmed.length < 32 && /^[0-9]/.test(trimmed)) {
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Synthesize the TLS-fingerprint status from three inputs. All three are
|
|
62
|
+
* passed explicitly so tests can cover every combination without touching
|
|
63
|
+
* the real environment. Production callers pass
|
|
64
|
+
* `classifyRuntimeFingerprint(typeof Bun !== 'undefined', probeBunVersion(), process.env)`.
|
|
65
|
+
*
|
|
66
|
+
* The `env` parameter is read-only — this function never mutates it.
|
|
67
|
+
*/
|
|
68
|
+
export function classifyRuntimeFingerprint(runningUnderBun, availableBunVersion, env, nodeVersion = process.version) {
|
|
69
|
+
if (runningUnderBun) {
|
|
70
|
+
// When we're under Bun, we expose the Bun version if globalThis.Bun.version
|
|
71
|
+
// is readable; we don't require a separate probe. The caller passes the
|
|
72
|
+
// resolved version string as `availableBunVersion` in the bun case.
|
|
73
|
+
const bunVer = availableBunVersion ?? 'unknown';
|
|
74
|
+
return {
|
|
75
|
+
status: 'bun-match',
|
|
76
|
+
runtime: 'bun',
|
|
77
|
+
runtimeVersion: bunVer,
|
|
78
|
+
detail: `Bun v${bunVer} — TLS fingerprint matches Claude Code`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (availableBunVersion !== undefined) {
|
|
82
|
+
const reason = env.DARIO_NO_BUN ? 'DARIO_NO_BUN' : 'unknown';
|
|
83
|
+
return {
|
|
84
|
+
status: 'bun-bypassed',
|
|
85
|
+
runtime: 'node',
|
|
86
|
+
runtimeVersion: nodeVersion,
|
|
87
|
+
availableBunVersion,
|
|
88
|
+
bypassReason: reason,
|
|
89
|
+
detail: `Node ${nodeVersion} — Bun v${availableBunVersion} on PATH but auto-relaunch bypassed (${reason})`,
|
|
90
|
+
hint: reason === 'DARIO_NO_BUN'
|
|
91
|
+
? 'Unset DARIO_NO_BUN to auto-relaunch under Bun on the next invocation.'
|
|
92
|
+
: 'Run dario fresh (no inherited DARIO_NO_BUN) so auto-relaunch can fire.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
status: 'node-only',
|
|
97
|
+
runtime: 'node',
|
|
98
|
+
runtimeVersion: nodeVersion,
|
|
99
|
+
detail: `Node ${nodeVersion} — Bun not installed; proxy-mode TLS fingerprint diverges from Claude Code`,
|
|
100
|
+
hint: 'Install Bun (https://bun.sh) so dario can auto-relaunch under it, or use shim mode ' +
|
|
101
|
+
'(`dario shim -- claude …`) which runs inside CC\'s own process and inherits its TLS stack.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Convenience wrapper that reads the current process state. doctor.ts
|
|
106
|
+
* calls this once; tests do not — they exercise classifyRuntimeFingerprint
|
|
107
|
+
* directly with synthetic inputs.
|
|
108
|
+
*/
|
|
109
|
+
export function detectRuntimeFingerprint() {
|
|
110
|
+
const bunGlobal = globalThis.Bun;
|
|
111
|
+
const runningUnderBun = typeof bunGlobal?.version === 'string';
|
|
112
|
+
if (runningUnderBun) {
|
|
113
|
+
return classifyRuntimeFingerprint(true, bunGlobal?.version, process.env);
|
|
114
|
+
}
|
|
115
|
+
const probed = probeBunVersion();
|
|
116
|
+
return classifyRuntimeFingerprint(false, probed, process.env);
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.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": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
|
|
24
|
-
"test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
|
|
24
|
+
"test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
|
|
25
25
|
"audit": "npm audit --production --audit-level=high",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"start": "node dist/cli.js",
|