@askalf/dario 3.4.1 → 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.d.ts +30 -14
- package/dist/cc-oauth-detect.js +85 -81
- package/dist/cc-template-data.json +1 -1
- package/dist/cc-template.js +61 -26
- package/dist/cli.js +17 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +98 -11
- package/package.json +4 -2
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 |
|
|
@@ -5,20 +5,32 @@
|
|
|
5
5
|
* (client_id, authorize URL, token URL, scopes). Eliminates the need to
|
|
6
6
|
* hardcode values that Anthropic rotates between CC releases.
|
|
7
7
|
*
|
|
8
|
-
* CC ships
|
|
8
|
+
* CC ships three OAuth config factories in one binary (dev/staging/prod),
|
|
9
|
+
* selected at runtime by an environment switch that is hardcoded to "prod"
|
|
10
|
+
* in shipped builds. Only the PROD block is live; "local" and "staging"
|
|
11
|
+
* are dead code paths.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* PROD block (the one we want):
|
|
14
|
+
* BASE_API_URL: "https://api.anthropic.com"
|
|
15
|
+
* CLAUDE_AI_AUTHORIZE_URL: "https://claude.com/cai/oauth/authorize"
|
|
16
|
+
* TOKEN_URL: "https://platform.claude.com/v1/oauth/token"
|
|
17
|
+
* CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
18
|
+
* OAUTH_FILE_SUFFIX: ""
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* LOCAL block (dead code in shipped builds — CC pointing at localhost:8000
|
|
21
|
+
* etc. as its own dev stack, NOT about "client uses a localhost callback"):
|
|
22
|
+
* BASE_API_URL: "http://localhost:8000"
|
|
23
|
+
* CLIENT_ID: "22422756-60c9-4084-8eb7-27705fd5cf9a"
|
|
24
|
+
* OAUTH_FILE_SUFFIX: "-local-oauth"
|
|
17
25
|
*
|
|
18
|
-
*
|
|
26
|
+
* Dario uses CC's own automatic OAuth flow — the prod client is registered
|
|
27
|
+
* with `http://localhost:${port}/callback` exactly as dario sends. (The
|
|
28
|
+
* "MANUAL_REDIRECT_URL" on platform.claude.com is only used when dario's
|
|
29
|
+
* local HTTP server can't bind a port; dario never hits that path.)
|
|
19
30
|
*
|
|
20
|
-
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache.json so
|
|
21
|
-
* startup only re-scans when the user upgrades Claude Code.
|
|
31
|
+
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v2.json so
|
|
32
|
+
* startup only re-scans when the user upgrades Claude Code. The -v2 suffix
|
|
33
|
+
* invalidates the v3.4.0-v3.4.2 caches that held the wrong (dev) client_id.
|
|
22
34
|
*/
|
|
23
35
|
export interface DetectedOAuthConfig {
|
|
24
36
|
clientId: string;
|
|
@@ -30,10 +42,14 @@ export interface DetectedOAuthConfig {
|
|
|
30
42
|
ccHash?: string;
|
|
31
43
|
}
|
|
32
44
|
/**
|
|
33
|
-
* Scan binary bytes for the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
45
|
+
* Scan binary bytes for the PROD OAuth config block.
|
|
46
|
+
*
|
|
47
|
+
* Anchors on `BASE_API_URL:"https://api.anthropic.com"` — this literal
|
|
48
|
+
* only appears inside the prod config object (`nh$`). The LOCAL-dev block
|
|
49
|
+
* uses `http://localhost:8000` for the same key, and there's no staging
|
|
50
|
+
* block present in shipped builds. Once we find the anchor, the CLIENT_ID,
|
|
51
|
+
* CLAUDE_AI_AUTHORIZE_URL, TOKEN_URL, and scopes all live within a ~1.5KB
|
|
52
|
+
* window after it.
|
|
37
53
|
*/
|
|
38
54
|
export declare function scanBinaryForOAuthConfig(buf: Buffer): Omit<DetectedOAuthConfig, 'source' | 'ccPath' | 'ccHash'> | null;
|
|
39
55
|
/**
|
package/dist/cc-oauth-detect.js
CHANGED
|
@@ -5,20 +5,32 @@
|
|
|
5
5
|
* (client_id, authorize URL, token URL, scopes). Eliminates the need to
|
|
6
6
|
* hardcode values that Anthropic rotates between CC releases.
|
|
7
7
|
*
|
|
8
|
-
* CC ships
|
|
8
|
+
* CC ships three OAuth config factories in one binary (dev/staging/prod),
|
|
9
|
+
* selected at runtime by an environment switch that is hardcoded to "prod"
|
|
10
|
+
* in shipped builds. Only the PROD block is live; "local" and "staging"
|
|
11
|
+
* are dead code paths.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* PROD block (the one we want):
|
|
14
|
+
* BASE_API_URL: "https://api.anthropic.com"
|
|
15
|
+
* CLAUDE_AI_AUTHORIZE_URL: "https://claude.com/cai/oauth/authorize"
|
|
16
|
+
* TOKEN_URL: "https://platform.claude.com/v1/oauth/token"
|
|
17
|
+
* CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
18
|
+
* OAUTH_FILE_SUFFIX: ""
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* LOCAL block (dead code in shipped builds — CC pointing at localhost:8000
|
|
21
|
+
* etc. as its own dev stack, NOT about "client uses a localhost callback"):
|
|
22
|
+
* BASE_API_URL: "http://localhost:8000"
|
|
23
|
+
* CLIENT_ID: "22422756-60c9-4084-8eb7-27705fd5cf9a"
|
|
24
|
+
* OAUTH_FILE_SUFFIX: "-local-oauth"
|
|
17
25
|
*
|
|
18
|
-
*
|
|
26
|
+
* Dario uses CC's own automatic OAuth flow — the prod client is registered
|
|
27
|
+
* with `http://localhost:${port}/callback` exactly as dario sends. (The
|
|
28
|
+
* "MANUAL_REDIRECT_URL" on platform.claude.com is only used when dario's
|
|
29
|
+
* local HTTP server can't bind a port; dario never hits that path.)
|
|
19
30
|
*
|
|
20
|
-
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache.json so
|
|
21
|
-
* startup only re-scans when the user upgrades Claude Code.
|
|
31
|
+
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v2.json so
|
|
32
|
+
* startup only re-scans when the user upgrades Claude Code. The -v2 suffix
|
|
33
|
+
* invalidates the v3.4.0-v3.4.2 caches that held the wrong (dev) client_id.
|
|
22
34
|
*/
|
|
23
35
|
import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
|
|
24
36
|
import { existsSync } from 'node:fs';
|
|
@@ -26,15 +38,29 @@ import { homedir, platform } from 'node:os';
|
|
|
26
38
|
import { join, dirname } from 'node:path';
|
|
27
39
|
import { createHash } from 'node:crypto';
|
|
28
40
|
// Last-resort fallback if CC binary can't be found or scanned.
|
|
29
|
-
// These values are the
|
|
41
|
+
// These values are the CC v2.1.104 PROD OAuth config, extracted from
|
|
42
|
+
// the `nh$` object in the shipped binary.
|
|
30
43
|
const FALLBACK = {
|
|
31
|
-
clientId: '
|
|
44
|
+
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
32
45
|
authorizeUrl: 'https://claude.com/cai/oauth/authorize',
|
|
33
46
|
tokenUrl: 'https://platform.claude.com/v1/oauth/token',
|
|
34
|
-
|
|
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',
|
|
35
57
|
source: 'fallback',
|
|
36
58
|
};
|
|
37
|
-
|
|
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');
|
|
38
64
|
function candidatePaths() {
|
|
39
65
|
const home = homedir();
|
|
40
66
|
if (platform() === 'win32') {
|
|
@@ -88,80 +114,58 @@ async function fingerprintBinary(path) {
|
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
116
|
/**
|
|
91
|
-
* Scan binary bytes for the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
117
|
+
* Scan binary bytes for the PROD OAuth config block.
|
|
118
|
+
*
|
|
119
|
+
* Anchors on `BASE_API_URL:"https://api.anthropic.com"` — this literal
|
|
120
|
+
* only appears inside the prod config object (`nh$`). The LOCAL-dev block
|
|
121
|
+
* uses `http://localhost:8000` for the same key, and there's no staging
|
|
122
|
+
* block present in shipped builds. Once we find the anchor, the CLIENT_ID,
|
|
123
|
+
* CLAUDE_AI_AUTHORIZE_URL, TOKEN_URL, and scopes all live within a ~1.5KB
|
|
124
|
+
* window after it.
|
|
95
125
|
*/
|
|
96
126
|
export function scanBinaryForOAuthConfig(buf) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// just `-local-oauth` bytes, but the config object serializes as
|
|
100
|
-
// `OAUTH_FILE_SUFFIX:"-local-oauth"` with the key+quote prefix, which is
|
|
101
|
-
// stable across minified CC builds.
|
|
102
|
-
const anchor = Buffer.from('OAUTH_FILE_SUFFIX:"-local-oauth"');
|
|
103
|
-
let anchorIdx = buf.indexOf(anchor);
|
|
104
|
-
// Fallback anchor — some builds may tokenize differently.
|
|
105
|
-
if (anchorIdx === -1) {
|
|
106
|
-
const looseAnchor = Buffer.from('"-local-oauth"');
|
|
107
|
-
anchorIdx = buf.indexOf(looseAnchor);
|
|
108
|
-
}
|
|
127
|
+
const anchor = Buffer.from('BASE_API_URL:"https://api.anthropic.com"');
|
|
128
|
+
const anchorIdx = buf.indexOf(anchor);
|
|
109
129
|
if (anchorIdx === -1)
|
|
110
130
|
return null;
|
|
111
|
-
// The
|
|
112
|
-
//
|
|
113
|
-
const windowStart =
|
|
114
|
-
const windowEnd = Math.min(buf.length, anchorIdx +
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!lastCid)
|
|
131
|
+
// The prod config object is laid out roughly as one line of minified JS.
|
|
132
|
+
// Take a generous window to be safe across minifier differences.
|
|
133
|
+
const windowStart = anchorIdx;
|
|
134
|
+
const windowEnd = Math.min(buf.length, anchorIdx + 2048);
|
|
135
|
+
const prodBlock = buf.slice(windowStart, windowEnd).toString('latin1');
|
|
136
|
+
const cidMatch = /CLIENT_ID\s*:\s*"([0-9a-f-]{36})"/i.exec(prodBlock);
|
|
137
|
+
if (!cidMatch || !cidMatch[1])
|
|
138
|
+
return null;
|
|
139
|
+
const clientId = cidMatch[1];
|
|
140
|
+
// Defensive: if we somehow matched the dev client_id, reject — the
|
|
141
|
+
// anchor should have put us in the prod block, but this guards against
|
|
142
|
+
// the block being laid out in an unexpected order across builds.
|
|
143
|
+
if (clientId === '22422756-60c9-4084-8eb7-27705fd5cf9a')
|
|
125
144
|
return null;
|
|
126
|
-
const clientId = lastCid;
|
|
127
|
-
// Authorize URL: CLAUDE_AI_AUTHORIZE_URL appears once in the binary.
|
|
128
|
-
const authAnchor = Buffer.from('CLAUDE_AI_AUTHORIZE_URL');
|
|
129
|
-
const authIdx = buf.indexOf(authAnchor);
|
|
130
145
|
let authorizeUrl = FALLBACK.authorizeUrl;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (m && m[1])
|
|
135
|
-
authorizeUrl = m[1];
|
|
136
|
-
}
|
|
137
|
-
// Token URL: TOKEN_URL — look for the one under platform.claude.com/.../oauth/token
|
|
138
|
-
const tokenAnchor = Buffer.from('TOKEN_URL');
|
|
139
|
-
let searchFrom = 0;
|
|
146
|
+
const authMatch = /CLAUDE_AI_AUTHORIZE_URL\s*:\s*"([^"]+)"/.exec(prodBlock);
|
|
147
|
+
if (authMatch && authMatch[1])
|
|
148
|
+
authorizeUrl = authMatch[1];
|
|
140
149
|
let tokenUrl = FALLBACK.tokenUrl;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const m = /"(user:profile(?:\s+user:[a-z_:]+)+)"/.exec(w);
|
|
161
|
-
if (m && m[1])
|
|
162
|
-
scopes = m[1];
|
|
163
|
-
}
|
|
164
|
-
return { clientId, authorizeUrl, tokenUrl, scopes };
|
|
150
|
+
const tokenMatch = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(prodBlock);
|
|
151
|
+
if (tokenMatch && tokenMatch[1])
|
|
152
|
+
tokenUrl = tokenMatch[1];
|
|
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 };
|
|
165
169
|
}
|
|
166
170
|
async function loadCache() {
|
|
167
171
|
try {
|
package/dist/cc-template.js
CHANGED
|
@@ -46,6 +46,16 @@ const TOOL_MAP = {
|
|
|
46
46
|
browse: { ccTool: 'WebFetch', translateArgs: (a) => ({ url: a.url || '' }) },
|
|
47
47
|
notebook: { ccTool: 'NotebookEdit' },
|
|
48
48
|
notebook_edit: { ccTool: 'NotebookEdit' },
|
|
49
|
+
// Additional client tool mappings
|
|
50
|
+
browser: { ccTool: 'WebFetch', translateArgs: (a) => ({ url: String(a.url || '') }) },
|
|
51
|
+
message: { ccTool: 'AskUserQuestion', translateArgs: (a) => ({ question: String(a.message || a.content || '') }) },
|
|
52
|
+
todo_read: { ccTool: 'TodoWrite', translateArgs: () => ({ todos: [] }) },
|
|
53
|
+
todo_write: { ccTool: 'TodoWrite', translateArgs: (a) => ({ todos: a.todos || [] }) },
|
|
54
|
+
notebook_read: { ccTool: 'NotebookEdit', translateArgs: (a) => ({ notebook_path: String(a.notebook_path || a.path || '') }) },
|
|
55
|
+
enter_plan_mode: { ccTool: 'EnterPlanMode' },
|
|
56
|
+
exit_plan_mode: { ccTool: 'ExitPlanMode' },
|
|
57
|
+
enter_worktree: { ccTool: 'EnterWorktree', translateArgs: (a) => ({ path: a.path }) },
|
|
58
|
+
exit_worktree: { ccTool: 'ExitWorktree' },
|
|
49
59
|
};
|
|
50
60
|
/**
|
|
51
61
|
* Build a CC-template request from a client request.
|
|
@@ -79,34 +89,47 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
79
89
|
const activeToolMap = new Map();
|
|
80
90
|
const unmappedTools = [];
|
|
81
91
|
if (clientTools && !opts.preserveTools) {
|
|
92
|
+
// Two passes so the unmapped-tool distributor can avoid colliding with
|
|
93
|
+
// CC tools the client already uses directly. Without this, a client
|
|
94
|
+
// sending both `WebSearch` and some unmapped tool like `memory_get`
|
|
95
|
+
// could have both forward-map to `WebSearch`, and the reverse map would
|
|
96
|
+
// then rewrite real `WebSearch` responses to the collided client name.
|
|
97
|
+
const claimedCC = new Set();
|
|
82
98
|
for (const tool of clientTools) {
|
|
83
99
|
const name = (tool.name || '').toLowerCase();
|
|
84
100
|
const mapping = TOOL_MAP[name];
|
|
85
101
|
if (mapping) {
|
|
86
102
|
activeToolMap.set(tool.name, mapping);
|
|
103
|
+
claimedCC.add(mapping.ccTool);
|
|
87
104
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
}
|
|
106
|
+
const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
107
|
+
for (const tool of clientTools) {
|
|
108
|
+
const name = (tool.name || '').toLowerCase();
|
|
109
|
+
if (TOOL_MAP[name])
|
|
110
|
+
continue;
|
|
111
|
+
unmappedTools.push(tool.name);
|
|
112
|
+
// Exclude CC tools the client already uses so we never create a
|
|
113
|
+
// two-client-names-to-one-CC-tool collision. If every fallback is
|
|
114
|
+
// claimed (rare: client already uses 6+ CC tools), fall back to the
|
|
115
|
+
// full pool and accept the ambiguity.
|
|
116
|
+
const pool = CC_FALLBACK_TOOLS.filter(t => !claimedCC.has(t));
|
|
117
|
+
const fallbackPool = pool.length > 0 ? pool : CC_FALLBACK_TOOLS;
|
|
118
|
+
const fallbackTool = fallbackPool[(unmappedTools.length - 1) % fallbackPool.length];
|
|
119
|
+
activeToolMap.set(tool.name, {
|
|
120
|
+
ccTool: fallbackTool,
|
|
121
|
+
translateArgs: (a) => {
|
|
122
|
+
switch (fallbackTool) {
|
|
123
|
+
case 'Bash': return { command: `echo "${JSON.stringify(a).slice(0, 200)}"` };
|
|
124
|
+
case 'Read': return { file_path: String(a.path || a.file || a.url || '/tmp/output') };
|
|
125
|
+
case 'Grep': return { pattern: String(a.query || a.pattern || a.search || '.'), path: '.' };
|
|
126
|
+
case 'Glob': return { pattern: String(a.pattern || a.glob || '*') };
|
|
127
|
+
case 'WebSearch': return { query: String(a.query || a.q || a.search || '') };
|
|
128
|
+
case 'WebFetch': return { url: String(a.url || a.uri || '') };
|
|
129
|
+
default: return a;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
110
133
|
}
|
|
111
134
|
}
|
|
112
135
|
// ── Remap tool_use and tool_result references in message history ──
|
|
@@ -232,14 +255,26 @@ export function reverseMapResponse(responseBody, toolMap) {
|
|
|
232
255
|
if (toolMap.size === 0)
|
|
233
256
|
return responseBody;
|
|
234
257
|
let result = responseBody;
|
|
235
|
-
// Build reverse map: CC tool name → original client tool name
|
|
258
|
+
// Build reverse map: CC tool name → original client tool name.
|
|
259
|
+
// Two passes so identity mappings (client sent a tool with the real CC
|
|
260
|
+
// name) claim their CC slot first and can never be overwritten by a
|
|
261
|
+
// non-identity entry. Without this, a collision between a direct
|
|
262
|
+
// `WebSearch` and an unmapped-tool fallback landing on `WebSearch` could
|
|
263
|
+
// rewrite the real search response to the wrong client name.
|
|
236
264
|
const reverseMap = new Map();
|
|
265
|
+
const identityClaimed = new Set();
|
|
237
266
|
for (const [clientName, mapping] of toolMap) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
reverseMap.set(mapping.ccTool, clientName);
|
|
267
|
+
if (clientName.toLowerCase() === mapping.ccTool.toLowerCase()) {
|
|
268
|
+
identityClaimed.add(mapping.ccTool);
|
|
241
269
|
}
|
|
242
270
|
}
|
|
271
|
+
for (const [clientName, mapping] of toolMap) {
|
|
272
|
+
if (clientName.toLowerCase() === mapping.ccTool.toLowerCase())
|
|
273
|
+
continue;
|
|
274
|
+
if (identityClaimed.has(mapping.ccTool))
|
|
275
|
+
continue;
|
|
276
|
+
reverseMap.set(mapping.ccTool, clientName);
|
|
277
|
+
}
|
|
243
278
|
for (const [ccName, clientName] of reverseMap) {
|
|
244
279
|
result = result.replace(new RegExp(`"name"\\s*:\\s*"${ccName}"`, 'g'), `"name":"${clientName}"`);
|
|
245
280
|
}
|
package/dist/cli.js
CHANGED
|
@@ -125,12 +125,22 @@ async function proxy() {
|
|
|
125
125
|
console.error('[dario] Invalid port. Must be 1-65535.');
|
|
126
126
|
process.exit(1);
|
|
127
127
|
}
|
|
128
|
+
// Bind address — accepts --host=<addr>; falls through to DARIO_HOST env
|
|
129
|
+
// var or the default of 127.0.0.1 inside startProxy. The sanity check
|
|
130
|
+
// here only rejects obviously bad shapes; real address validation
|
|
131
|
+
// happens when the OS tries to bind.
|
|
132
|
+
const hostArg = args.find(a => a.startsWith('--host='));
|
|
133
|
+
const host = hostArg ? hostArg.split('=')[1] : undefined;
|
|
134
|
+
if (host !== undefined && !/^[a-zA-Z0-9._:-]+$/.test(host)) {
|
|
135
|
+
console.error('[dario] Invalid --host. Must be an IP address or hostname.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
128
138
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
129
139
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
130
140
|
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
131
141
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
132
142
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
133
|
-
await startProxy({ port, verbose, model, passthrough, preserveTools });
|
|
143
|
+
await startProxy({ port, host, verbose, model, passthrough, preserveTools });
|
|
134
144
|
}
|
|
135
145
|
async function help() {
|
|
136
146
|
console.log(`
|
|
@@ -151,6 +161,12 @@ async function help() {
|
|
|
151
161
|
--passthrough, --thin Thin proxy — OAuth swap only, no injection
|
|
152
162
|
--preserve-tools Keep client tool schemas (for agents with custom tools)
|
|
153
163
|
--port=PORT Port to listen on (default: 3456)
|
|
164
|
+
--host=ADDRESS Address to bind to (default: 127.0.0.1)
|
|
165
|
+
Use 0.0.0.0 to accept connections from other machines.
|
|
166
|
+
Alternatively set DARIO_HOST env var.
|
|
167
|
+
When binding non-loopback, also set DARIO_API_KEY
|
|
168
|
+
so unauthenticated LAN hosts can't proxy through
|
|
169
|
+
your OAuth subscription.
|
|
154
170
|
--verbose, -v Log all requests
|
|
155
171
|
|
|
156
172
|
Quick start:
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -13,7 +13,16 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts
|
|
|
13
13
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
14
14
|
const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
|
|
15
15
|
const MAX_CONCURRENT = 10; // Max concurrent upstream requests
|
|
16
|
-
const
|
|
16
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
17
|
+
// A host is "loopback" if it's one of the well-known localhost literals.
|
|
18
|
+
// Used to decide whether to warn at startup about binding to a reachable
|
|
19
|
+
// interface — binding anywhere else means other machines can reach the
|
|
20
|
+
// proxy and should only be done with DARIO_API_KEY set.
|
|
21
|
+
function isLoopbackHost(host) {
|
|
22
|
+
if (host === '127.0.0.1' || host === '::1' || host === 'localhost')
|
|
23
|
+
return true;
|
|
24
|
+
return host.startsWith('127.');
|
|
25
|
+
}
|
|
17
26
|
// Simple semaphore for concurrency control
|
|
18
27
|
class Semaphore {
|
|
19
28
|
max;
|
|
@@ -308,6 +317,7 @@ function enrich429(body, headers) {
|
|
|
308
317
|
}
|
|
309
318
|
export async function startProxy(opts = {}) {
|
|
310
319
|
const port = opts.port ?? DEFAULT_PORT;
|
|
320
|
+
const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
|
|
311
321
|
const verbose = opts.verbose ?? false;
|
|
312
322
|
const passthrough = opts.passthrough ?? false;
|
|
313
323
|
// Verify auth before starting
|
|
@@ -354,7 +364,11 @@ export async function startProxy(opts = {}) {
|
|
|
354
364
|
// Optional proxy authentication — pre-encode key buffer for performance
|
|
355
365
|
const apiKey = process.env.DARIO_API_KEY;
|
|
356
366
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
357
|
-
|
|
367
|
+
// CORS origin defaults to the localhost URL the proxy is served at. Users
|
|
368
|
+
// binding to a non-loopback address (e.g. a Tailscale interface) can
|
|
369
|
+
// override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
|
|
370
|
+
// dario over the mesh will be blocked by their browser's CORS check.
|
|
371
|
+
const corsOrigin = process.env.DARIO_CORS_ORIGIN || `http://localhost:${port}`;
|
|
358
372
|
// Security headers for all responses
|
|
359
373
|
const SECURITY_HEADERS = {
|
|
360
374
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -445,6 +459,10 @@ export async function startProxy(opts = {}) {
|
|
|
445
459
|
}
|
|
446
460
|
// Proxy to Anthropic (with concurrency control)
|
|
447
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;
|
|
448
466
|
try {
|
|
449
467
|
const accessToken = await getAccessToken();
|
|
450
468
|
// Read request body with size limit and timeout (prevents slow-loris)
|
|
@@ -548,11 +566,33 @@ export async function startProxy(opts = {}) {
|
|
|
548
566
|
// CC sends 600 on first request per session. With rotation, every request is "first"
|
|
549
567
|
'x-stainless-timeout': '600',
|
|
550
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);
|
|
551
591
|
let upstream = await fetch(targetBase, {
|
|
552
592
|
method: req.method ?? 'POST',
|
|
553
593
|
headers,
|
|
554
594
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
555
|
-
signal:
|
|
595
|
+
signal: upstreamAbort.signal,
|
|
556
596
|
});
|
|
557
597
|
// Auto-retry without context-1m if it triggers a long-context billing error.
|
|
558
598
|
// Anthropic returns this as either 400 ("long context beta is not yet available
|
|
@@ -576,7 +616,7 @@ export async function startProxy(opts = {}) {
|
|
|
576
616
|
method: req.method ?? 'POST',
|
|
577
617
|
headers: retryHeaders,
|
|
578
618
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
579
|
-
signal:
|
|
619
|
+
signal: upstreamAbort.signal,
|
|
580
620
|
});
|
|
581
621
|
// Use the retry response from here on — peeked body is now stale
|
|
582
622
|
upstream = retry;
|
|
@@ -736,12 +776,42 @@ export async function startProxy(opts = {}) {
|
|
|
736
776
|
}
|
|
737
777
|
}
|
|
738
778
|
catch (err) {
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
+
}
|
|
743
806
|
}
|
|
744
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);
|
|
745
815
|
semaphore.release();
|
|
746
816
|
}
|
|
747
817
|
});
|
|
@@ -754,22 +824,39 @@ export async function startProxy(opts = {}) {
|
|
|
754
824
|
}
|
|
755
825
|
process.exit(1);
|
|
756
826
|
});
|
|
757
|
-
server.listen(port,
|
|
827
|
+
server.listen(port, host, () => {
|
|
758
828
|
const modeLine = passthrough
|
|
759
829
|
? 'Mode: passthrough (OAuth swap only, no injection)'
|
|
760
830
|
: `OAuth: ${status.status} (expires in ${status.expiresIn})`;
|
|
761
831
|
const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
|
|
832
|
+
// Display URL uses `localhost` for loopback binds and the literal host
|
|
833
|
+
// for exposed binds, so the printed URL is the one a client would
|
|
834
|
+
// actually use to reach the proxy.
|
|
835
|
+
const displayHost = isLoopbackHost(host) ? 'localhost' : host;
|
|
762
836
|
console.log('');
|
|
763
|
-
console.log(` dario — http
|
|
837
|
+
console.log(` dario — http://${displayHost}:${port}`);
|
|
764
838
|
console.log('');
|
|
765
839
|
console.log(' Your Claude subscription is now an API.');
|
|
766
840
|
console.log('');
|
|
767
841
|
console.log(' Usage:');
|
|
768
|
-
console.log(` ANTHROPIC_BASE_URL=http
|
|
842
|
+
console.log(` ANTHROPIC_BASE_URL=http://${displayHost}:${port}`);
|
|
769
843
|
console.log(' ANTHROPIC_API_KEY=dario');
|
|
770
844
|
console.log('');
|
|
771
845
|
console.log(` ${modeLine}`);
|
|
772
846
|
console.log(` ${modelLine}`);
|
|
847
|
+
if (!isLoopbackHost(host)) {
|
|
848
|
+
console.log('');
|
|
849
|
+
console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
|
|
850
|
+
if (!apiKey) {
|
|
851
|
+
console.log(' DARIO_API_KEY is not set. Any host that can reach this port can');
|
|
852
|
+
console.log(' proxy requests through your OAuth subscription. Set DARIO_API_KEY');
|
|
853
|
+
console.log(' before exposing dario beyond loopback.');
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.log(' DARIO_API_KEY is set — clients must send x-api-key or Authorization');
|
|
857
|
+
console.log(' to be accepted.');
|
|
858
|
+
}
|
|
859
|
+
}
|
|
773
860
|
console.log('');
|
|
774
861
|
});
|
|
775
862
|
// Session presence heartbeat — keeps the OAuth session marked active
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.4",
|
|
4
4
|
"description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"dev": "tsx src/cli.ts",
|
|
28
28
|
"e2e": "node test/e2e.mjs",
|
|
29
|
-
"compat": "node test/compat.mjs"
|
|
29
|
+
"compat": "node test/compat.mjs",
|
|
30
|
+
"lint:pkg": "node scripts/check-package-json.mjs",
|
|
31
|
+
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"claude",
|