@askalf/dario 3.4.3 → 3.4.4
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 -11
- package/dist/cc-oauth-detect.js +31 -17
- package/dist/proxy.js +62 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -408,13 +408,13 @@ Anthropic periodically rotates the OAuth `client_id`, authorize URL, token URL,
|
|
|
408
408
|
|
|
409
409
|
Dario scans the installed CC binary at startup and extracts the current config directly:
|
|
410
410
|
|
|
411
|
-
- **Anchor**: `
|
|
412
|
-
- **Extracted**: `CLIENT_ID`, `CLAUDE_AI_AUTHORIZE_URL`, `TOKEN_URL`, and the full `user:*` scope string.
|
|
413
|
-
- **Cached**: Results stored at `~/.dario/cc-oauth-cache.json` keyed by binary fingerprint (first 64KB sha256 + size + mtime). Cold scan ~500ms, cache hit ~5ms. Re-scans only when CC is upgraded.
|
|
414
|
-
- **Fallback**: If CC is not installed or scanning fails, dario uses
|
|
411
|
+
- **Anchor**: `BASE_API_URL:"https://api.anthropic.com"` — this literal appears only inside CC's live prod OAuth config block, so the scanner reliably lands in the right object even when the minifier reorders fields across CC releases.
|
|
412
|
+
- **Extracted**: `CLIENT_ID`, `CLAUDE_AI_AUTHORIZE_URL`, `TOKEN_URL`, and the full `user:*` scope string. A defensive check rejects any scan result that matches a known-dead internal client_id.
|
|
413
|
+
- **Cached**: Results stored at `~/.dario/cc-oauth-cache-v2.json` keyed by binary fingerprint (first 64KB sha256 + size + mtime). Cold scan ~500ms, cache hit ~5ms. Re-scans only when CC is upgraded.
|
|
414
|
+
- **Fallback**: If CC is not installed or scanning fails, dario uses the CC 2.1.104 prod config values hardcoded in-tree. No user action needed.
|
|
415
415
|
- **Override**: Set `DARIO_CC_PATH=/path/to/claude` to point dario at a non-standard CC binary location.
|
|
416
416
|
|
|
417
|
-
CC ships
|
|
417
|
+
CC ships three OAuth config factories (`local`, `staging`, `prod`) in one binary, selected at runtime by a function that is hardcoded to `prod` in shipped builds. Only the prod block is live; the other two are dead code paths CC uses when pointing at internal dev/staging infrastructure. The scanner targets the prod block specifically.
|
|
418
418
|
|
|
419
419
|
End-to-end verification lives at [`test/oauth-detector.mjs`](test/oauth-detector.mjs).
|
|
420
420
|
|
|
@@ -439,8 +439,10 @@ End-to-end verification lives at [`test/oauth-detector.mjs`](test/oauth-detector
|
|
|
439
439
|
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC tools | off |
|
|
440
440
|
| `--model=MODEL` | Force a model (`opus`, `sonnet`, `haiku`, or full ID) | passthrough |
|
|
441
441
|
| `--port=PORT` | Port to listen on | `3456` |
|
|
442
|
+
| `--host=ADDRESS` / `DARIO_HOST` | Bind address. Use `0.0.0.0` to accept LAN connections, or a specific IP to bind selectively (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
|
|
442
443
|
| `--verbose` / `-v` | Log every request | off |
|
|
443
|
-
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` or `Authorization: Bearer` | unset (open) |
|
|
444
|
+
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` or `Authorization: Bearer`. **Required** when `--host` binds to anything other than loopback. | unset (open) |
|
|
445
|
+
| `DARIO_CORS_ORIGIN` | Override the browser CORS `Access-Control-Allow-Origin`. Useful for browser-based clients (open-webui, librechat) connecting over a mesh network. | `http://localhost:${port}` |
|
|
444
446
|
| `DARIO_NO_BUN` | Disable automatic Bun relaunch (stay on Node.js) | unset |
|
|
445
447
|
| `DARIO_MIN_INTERVAL_MS` | Minimum ms between requests (rate governor) | `500` |
|
|
446
448
|
| `DARIO_CC_PATH` | Override path to Claude Code binary for OAuth detection | auto-detect |
|
|
@@ -506,9 +508,9 @@ curl http://localhost:3456/health
|
|
|
506
508
|
|---------|---------------------|
|
|
507
509
|
| Credential storage | Reads from Claude Code (`~/.claude/.credentials.json`) or its own store (`~/.dario/credentials.json`) with `0600` permissions |
|
|
508
510
|
| OAuth flow | PKCE (Proof Key for Code Exchange) — no client secret needed |
|
|
509
|
-
| OAuth config source | Auto-detected from local CC binary at runtime; cached at `~/.dario/cc-oauth-cache.json`. Detector reads binary in read-only mode, never modifies it. |
|
|
511
|
+
| OAuth config source | Auto-detected from local CC binary at runtime; cached at `~/.dario/cc-oauth-cache-v2.json`. Detector reads binary in read-only mode, never modifies it. |
|
|
510
512
|
| Token exposure | Tokens never logged; redacted from all error messages. |
|
|
511
|
-
| Network binding | Binds
|
|
513
|
+
| Network binding | Binds to `127.0.0.1` by default. Override with `--host` / `DARIO_HOST` for mesh/LAN use; dario refuses to treat non-loopback binds as safe and requires you to set `DARIO_API_KEY` to avoid an unauthenticated LAN-reachable proxy. Upstream traffic goes only to `api.anthropic.com` over HTTPS. |
|
|
512
514
|
| Auth timing | `timingSafeEqual` used for `DARIO_API_KEY` comparison. |
|
|
513
515
|
| SSRF protection | Only `/v1/messages` and `/v1/complete` are proxied upstream — hardcoded allowlist. |
|
|
514
516
|
| Body size | 10MB hard cap per request. 30s read timeout prevents slow-loris. |
|
|
@@ -571,7 +573,7 @@ Optional but recommended. If [Bun](https://bun.sh) is installed, dario auto-rela
|
|
|
571
573
|
Dario auto-refreshes tokens 30 minutes before expiry. You should never see an auth error in normal use. If something goes wrong, `dario refresh` forces an immediate refresh.
|
|
572
574
|
|
|
573
575
|
**What happens when Anthropic rotates the OAuth client_id or URL?**
|
|
574
|
-
Dario auto-detects OAuth config from your installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next startup — no dario release needed. The detector is cached at `~/.dario/cc-oauth-cache.json` and only re-scans when the binary fingerprint changes. If CC isn't installed, dario falls back to
|
|
576
|
+
Dario auto-detects OAuth config from your installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next startup — no dario release needed. The detector is cached at `~/.dario/cc-oauth-cache-v2.json` and only re-scans when the binary fingerprint changes. If CC isn't installed, dario falls back to hardcoded CC 2.1.104 prod values.
|
|
575
577
|
|
|
576
578
|
**I'm hitting rate limits. What do I do?**
|
|
577
579
|
Claude subscriptions have rolling 5-hour and 7-day usage windows. Check your utilization with Claude Code's `/usage` command or the [statusline](https://code.claude.com/docs/en/statusline). Rate limit errors from dario include utilization percentages and reset times so you can see exactly when capacity returns.
|
|
@@ -582,7 +584,7 @@ If you're running a multi-agent workload and consistently hitting limits, [askal
|
|
|
582
584
|
Claude subscriptions have rolling 5-hour and 7-day usage windows shared across claude.ai and Claude Code. See [Anthropic's docs](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) for details.
|
|
583
585
|
|
|
584
586
|
**Can I run this on a server?**
|
|
585
|
-
Dario binds to localhost by default. For server use, handle the initial login on a machine with a browser, then copy `~/.claude/.credentials.json` (or `~/.dario/credentials.json`) to your server. Auto-refresh will keep it alive from there.
|
|
587
|
+
Dario binds to localhost by default. For server use, handle the initial login on a machine with a browser, then copy `~/.claude/.credentials.json` (or `~/.dario/credentials.json`) to your server. Auto-refresh will keep it alive from there. If you want dario reachable from other machines on the same LAN or a Tailscale mesh, pass `--host=0.0.0.0` (or a specific interface IP) and set `DARIO_API_KEY` to gate access.
|
|
586
588
|
|
|
587
589
|
**Why "dario"?**
|
|
588
590
|
Named after [Dario Amodei](https://en.wikipedia.org/wiki/Dario_Amodei), CEO of Anthropic.
|
|
@@ -622,7 +624,7 @@ Dario handles your OAuth tokens. Here's why you can trust it:
|
|
|
622
624
|
| **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
|
|
623
625
|
| **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
|
|
624
626
|
| **Credential handling** | Tokens never logged, redacted from errors, stored with 0600 permissions |
|
|
625
|
-
| **Network scope** | Binds to 127.0.0.1
|
|
627
|
+
| **Network scope** | Binds to 127.0.0.1 by default; `--host` / `DARIO_HOST` allows LAN/mesh exposure with `DARIO_API_KEY` gating. Upstream traffic goes exclusively to `api.anthropic.com` over HTTPS |
|
|
626
628
|
| **No telemetry** | Zero analytics, tracking, or data collection of any kind |
|
|
627
629
|
| **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
|
|
628
630
|
| **Branch protection** | CI must pass before merge. CODEOWNERS enforces review |
|
package/dist/cc-oauth-detect.js
CHANGED
|
@@ -44,12 +44,23 @@ const FALLBACK = {
|
|
|
44
44
|
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
45
45
|
authorizeUrl: 'https://claude.com/cai/oauth/authorize',
|
|
46
46
|
tokenUrl: 'https://platform.claude.com/v1/oauth/token',
|
|
47
|
-
|
|
47
|
+
// Scopes are the full `n36` union from the CC binary, which is the value
|
|
48
|
+
// sent during a normal `claude login` (non-setup-token) flow. In CC's
|
|
49
|
+
// source: `let D = f ? [TI] : n36` where `f = inferenceOnly` (true only
|
|
50
|
+
// for `claude setup-token`). Normal interactive login uses the 6-scope
|
|
51
|
+
// union including `org:create_api_key` — even though that scope is named
|
|
52
|
+
// "Console-only" by convention, CC's own login flow requests it up front.
|
|
53
|
+
// Earlier dario versions (3.2.7 through 3.4.3) dropped `org:create_api_key`
|
|
54
|
+
// from the list based on a misread of the name; the dev-only client_id
|
|
55
|
+
// was lenient enough to accept the shorter list, the prod client_id is not.
|
|
56
|
+
scopes: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
|
|
48
57
|
source: 'fallback',
|
|
49
58
|
};
|
|
50
|
-
// -
|
|
51
|
-
// (
|
|
52
|
-
|
|
59
|
+
// -v3 suffix invalidates v3.4.3 caches that were populated with the wrong
|
|
60
|
+
// 4-scope list (the scanner's regex matched a help-message string literal
|
|
61
|
+
// in the CC binary instead of the real scope array). See the scanner's
|
|
62
|
+
// scope handling below for why scope detection is no longer attempted.
|
|
63
|
+
const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v3.json');
|
|
53
64
|
function candidatePaths() {
|
|
54
65
|
const home = homedir();
|
|
55
66
|
if (platform() === 'win32') {
|
|
@@ -139,19 +150,22 @@ export function scanBinaryForOAuthConfig(buf) {
|
|
|
139
150
|
const tokenMatch = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(prodBlock);
|
|
140
151
|
if (tokenMatch && tokenMatch[1])
|
|
141
152
|
tokenUrl = tokenMatch[1];
|
|
142
|
-
// Scopes
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// Scopes are NOT detected from the binary. Previous versions of this
|
|
154
|
+
// scanner anchored on `"user:profile ` and regex-captured the first
|
|
155
|
+
// contiguous quoted run of scopes, but that anchor matches an error/help
|
|
156
|
+
// message string literal (used by `claude setup-token` error output) that
|
|
157
|
+
// contains only 4 of the 6 actual scopes. The real scope array is stored
|
|
158
|
+
// as a constant-reference array — `dY8 = [B9H, TI, "user:sessions:...", ...]`
|
|
159
|
+
// — where the first two elements are minified variable references, not
|
|
160
|
+
// literal strings, so no regex can reliably extract the full list. And the
|
|
161
|
+
// runtime-computed union `n36` only exists after `Array.from(new Set(...))`
|
|
162
|
+
// executes, which we can't evaluate from a static scan.
|
|
163
|
+
//
|
|
164
|
+
// Given that scopes rarely change across CC releases (Anthropic adds or
|
|
165
|
+
// removes maybe one per major version), hardcoding them in FALLBACK is
|
|
166
|
+
// more reliable than scanning. If Anthropic changes the scope list, the
|
|
167
|
+
// fix is a one-line FALLBACK update in a dario release.
|
|
168
|
+
return { clientId, authorizeUrl, tokenUrl, scopes: FALLBACK.scopes };
|
|
155
169
|
}
|
|
156
170
|
async function loadCache() {
|
|
157
171
|
try {
|
package/dist/proxy.js
CHANGED
|
@@ -459,6 +459,10 @@ export async function startProxy(opts = {}) {
|
|
|
459
459
|
}
|
|
460
460
|
// Proxy to Anthropic (with concurrency control)
|
|
461
461
|
await semaphore.acquire();
|
|
462
|
+
// Hoisted so the finally block can clean up whatever was set.
|
|
463
|
+
let upstreamTimeout = null;
|
|
464
|
+
let onClientClose = null;
|
|
465
|
+
let upstreamAbortReason = null;
|
|
462
466
|
try {
|
|
463
467
|
const accessToken = await getAccessToken();
|
|
464
468
|
// Read request body with size limit and timeout (prevents slow-loris)
|
|
@@ -562,11 +566,33 @@ export async function startProxy(opts = {}) {
|
|
|
562
566
|
// CC sends 600 on first request per session. With rotation, every request is "first"
|
|
563
567
|
'x-stainless-timeout': '600',
|
|
564
568
|
};
|
|
569
|
+
// Client-disconnect abort: if the client drops the connection before
|
|
570
|
+
// we've finished sending the response, we abort the upstream fetch so
|
|
571
|
+
// Anthropic stops generating (and billing) a response nobody will
|
|
572
|
+
// read. Also carries the 5-minute upstream timeout via the same
|
|
573
|
+
// controller, so a single signal covers both cancellation reasons.
|
|
574
|
+
const upstreamAbort = new AbortController();
|
|
575
|
+
upstreamTimeout = setTimeout(() => {
|
|
576
|
+
if (!upstreamAbort.signal.aborted) {
|
|
577
|
+
upstreamAbortReason = 'timeout';
|
|
578
|
+
upstreamAbort.abort();
|
|
579
|
+
}
|
|
580
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
581
|
+
onClientClose = () => {
|
|
582
|
+
// 'close' fires on both normal teardown and client disconnect.
|
|
583
|
+
// We only want to abort if we haven't finished our response yet —
|
|
584
|
+
// normal teardown happens AFTER res.writableEnded becomes true.
|
|
585
|
+
if (!res.writableEnded && !upstreamAbort.signal.aborted) {
|
|
586
|
+
upstreamAbortReason = 'client_closed';
|
|
587
|
+
upstreamAbort.abort();
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
req.on('close', onClientClose);
|
|
565
591
|
let upstream = await fetch(targetBase, {
|
|
566
592
|
method: req.method ?? 'POST',
|
|
567
593
|
headers,
|
|
568
594
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
569
|
-
signal:
|
|
595
|
+
signal: upstreamAbort.signal,
|
|
570
596
|
});
|
|
571
597
|
// Auto-retry without context-1m if it triggers a long-context billing error.
|
|
572
598
|
// Anthropic returns this as either 400 ("long context beta is not yet available
|
|
@@ -590,7 +616,7 @@ export async function startProxy(opts = {}) {
|
|
|
590
616
|
method: req.method ?? 'POST',
|
|
591
617
|
headers: retryHeaders,
|
|
592
618
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
593
|
-
signal:
|
|
619
|
+
signal: upstreamAbort.signal,
|
|
594
620
|
});
|
|
595
621
|
// Use the retry response from here on — peeked body is now stale
|
|
596
622
|
upstream = retry;
|
|
@@ -750,12 +776,42 @@ export async function startProxy(opts = {}) {
|
|
|
750
776
|
}
|
|
751
777
|
}
|
|
752
778
|
catch (err) {
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
779
|
+
// Differentiate the three failure modes so each gets the right
|
|
780
|
+
// response (and so we don't spam logs when clients simply drop).
|
|
781
|
+
if (upstreamAbortReason === 'client_closed') {
|
|
782
|
+
if (verbose)
|
|
783
|
+
console.log(`[dario] #${requestCount} aborted (client disconnected)`);
|
|
784
|
+
}
|
|
785
|
+
else if (upstreamAbortReason === 'timeout') {
|
|
786
|
+
console.error(`[dario] #${requestCount} upstream timeout after ${UPSTREAM_TIMEOUT_MS / 1000}s`);
|
|
787
|
+
if (!res.headersSent) {
|
|
788
|
+
res.writeHead(504, JSON_HEADERS);
|
|
789
|
+
res.end(JSON.stringify({ error: 'Upstream timeout', message: `Anthropic did not respond within ${UPSTREAM_TIMEOUT_MS / 1000}s` }));
|
|
790
|
+
}
|
|
791
|
+
else if (!res.writableEnded) {
|
|
792
|
+
res.end();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Log full error server-side, return generic message to client
|
|
797
|
+
console.error('[dario] Proxy error:', sanitizeError(err));
|
|
798
|
+
if (!res.headersSent) {
|
|
799
|
+
res.writeHead(502, JSON_HEADERS);
|
|
800
|
+
res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
|
|
801
|
+
}
|
|
802
|
+
else if (!res.writableEnded) {
|
|
803
|
+
res.end();
|
|
804
|
+
}
|
|
805
|
+
}
|
|
757
806
|
}
|
|
758
807
|
finally {
|
|
808
|
+
// Always clean up the upstream-abort plumbing if it was set up. The
|
|
809
|
+
// setup happens after the body-read phase, so on fast-path errors
|
|
810
|
+
// (413, body read timeout) these may still be null — guard accordingly.
|
|
811
|
+
if (upstreamTimeout !== null)
|
|
812
|
+
clearTimeout(upstreamTimeout);
|
|
813
|
+
if (onClientClose !== null)
|
|
814
|
+
req.off('close', onClientClose);
|
|
759
815
|
semaphore.release();
|
|
760
816
|
}
|
|
761
817
|
});
|