@askalf/dario 1.1.3 → 1.2.1
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 +58 -18
- package/dist/cli.js +33 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oauth.d.ts +3 -11
- package/dist/oauth.js +98 -39
- package/dist/proxy.js +41 -10
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
<h1 align="center">dario</h1>
|
|
3
3
|
<p align="center"><strong>Use your Claude subscription as an API.</strong></p>
|
|
4
4
|
<p align="center">
|
|
5
|
-
|
|
5
|
+
No API key needed. Your Claude Max/Pro subscription becomes a local API endpoint<br/>that any tool, SDK, or framework can use.
|
|
6
6
|
</p>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/v/@askalf/dario?color=blue" alt="npm version"></a>
|
|
11
|
+
<a href="https://github.com/askalf/dario/actions/workflows/ci.yml"><img src="https://github.com/askalf/dario/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
12
|
+
<a href="https://github.com/askalf/dario/actions/workflows/codeql.yml"><img src="https://github.com/askalf/dario/actions/workflows/codeql.yml/badge.svg" alt="CodeQL"></a>
|
|
13
|
+
<a href="https://github.com/askalf/dario/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@askalf/dario" alt="License"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario" alt="Downloads"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
9
17
|
<p align="center">
|
|
10
18
|
<a href="#quick-start">Quick Start</a> •
|
|
11
19
|
<a href="#how-it-works">How It Works</a> •
|
|
@@ -17,8 +25,7 @@
|
|
|
17
25
|
---
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
|
-
npx @askalf/dario login #
|
|
21
|
-
npx @askalf/dario proxy # start local API on :3456
|
|
28
|
+
npx @askalf/dario login # detects Claude Code credentials, starts proxy
|
|
22
29
|
|
|
23
30
|
# now use it from anywhere
|
|
24
31
|
export ANTHROPIC_BASE_URL=http://localhost:3456
|
|
@@ -33,12 +40,18 @@ That's it. Any tool that speaks the Anthropic API now uses your subscription.
|
|
|
33
40
|
|
|
34
41
|
You pay $100-200/mo for Claude Max or Pro. But that subscription only works on claude.ai and Claude Code. If you want to use Claude with **any other tool** — OpenClaw, Cursor, Continue, Aider, your own scripts — you need a separate API key with separate billing.
|
|
35
42
|
|
|
36
|
-
**Note:** Claude subscriptions have [usage limits](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) that reset on rolling 5-hour and 7-day windows. When exceeded, Opus and Sonnet may return 429 errors while Haiku continues working. Use `--cli` mode to route through Claude Code's binary, which is not affected by these limits.
|
|
43
|
+
**Note:** Claude subscriptions have [usage limits](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) that reset on rolling 5-hour and 7-day windows. When exceeded, Opus and Sonnet may return 429 errors while Haiku continues working. You can check your utilization via Claude Code's `/usage` command or [statusline](https://code.claude.com/docs/en/statusline). Use `--cli` mode to route through Claude Code's binary, which is not affected by these limits.
|
|
37
44
|
|
|
38
45
|
**dario fixes this.** It creates a local proxy that translates API key auth into your subscription's OAuth tokens — and with `--cli` mode, routes through the Claude Code binary for uninterrupted access.
|
|
39
46
|
|
|
40
47
|
## Quick Start
|
|
41
48
|
|
|
49
|
+
### Prerequisites
|
|
50
|
+
|
|
51
|
+
[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed and logged in (recommended). Dario detects your existing Claude Code credentials automatically.
|
|
52
|
+
|
|
53
|
+
If Claude Code isn't installed, dario runs its own OAuth flow — opens your browser, you authorize, done.
|
|
54
|
+
|
|
42
55
|
### Install
|
|
43
56
|
|
|
44
57
|
```bash
|
|
@@ -49,7 +62,6 @@ Or use npx (no install needed):
|
|
|
49
62
|
|
|
50
63
|
```bash
|
|
51
64
|
npx @askalf/dario login
|
|
52
|
-
npx @askalf/dario proxy
|
|
53
65
|
```
|
|
54
66
|
|
|
55
67
|
### Login
|
|
@@ -58,7 +70,8 @@ npx @askalf/dario proxy
|
|
|
58
70
|
dario login
|
|
59
71
|
```
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
- **With Claude Code installed:** Detects your credentials automatically and starts the proxy. No browser needed.
|
|
74
|
+
- **Without Claude Code:** Opens your browser to Claude's OAuth page. Authorize, and dario captures the token automatically via a local callback server. Then run `dario proxy` to start the server.
|
|
62
75
|
|
|
63
76
|
### Start the proxy
|
|
64
77
|
|
|
@@ -115,8 +128,6 @@ Backend: Claude CLI (bypasses rate limits)
|
|
|
115
128
|
Model: claude-opus-4-6 (all requests)
|
|
116
129
|
```
|
|
117
130
|
|
|
118
|
-
**Requirements:** Claude Code must be installed (`npm install -g @anthropic-ai/claude-code` or already installed via the desktop app).
|
|
119
|
-
|
|
120
131
|
**Trade-offs vs direct API mode:**
|
|
121
132
|
|
|
122
133
|
| | Direct API (default) | CLI Backend (`--cli`) |
|
|
@@ -249,7 +260,7 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
|
|
|
249
260
|
└──────────┘ └─────────────────┘ └──────────────────┘
|
|
250
261
|
```
|
|
251
262
|
|
|
252
|
-
1. **`dario login`** —
|
|
263
|
+
1. **`dario login`** — Detects your existing Claude Code credentials (`~/.claude/.credentials.json`) and starts the proxy automatically. If Claude Code isn't installed, runs a PKCE OAuth flow with a local callback server to capture the token automatically.
|
|
253
264
|
|
|
254
265
|
2. **`dario proxy`** — Starts an HTTP server on localhost that implements the Anthropic Messages API. In direct mode, it swaps your API key for an OAuth bearer token. In CLI mode, it routes through the Claude Code binary.
|
|
255
266
|
|
|
@@ -259,7 +270,7 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
|
|
|
259
270
|
|
|
260
271
|
| Command | Description |
|
|
261
272
|
|---------|-------------|
|
|
262
|
-
| `dario login` |
|
|
273
|
+
| `dario login` | Detect credentials and start proxy |
|
|
263
274
|
| `dario proxy` | Start the local API proxy |
|
|
264
275
|
| `dario status` | Check if your token is healthy |
|
|
265
276
|
| `dario refresh` | Force an immediate token refresh |
|
|
@@ -319,11 +330,11 @@ curl http://localhost:3456/health
|
|
|
319
330
|
|
|
320
331
|
| Concern | How dario handles it |
|
|
321
332
|
|---------|---------------------|
|
|
322
|
-
| Credential storage | `~/.dario/credentials.json` with `0600` permissions
|
|
333
|
+
| Credential storage | Reads from Claude Code (`~/.claude/.credentials.json`) or its own store (`~/.dario/credentials.json`) with `0600` permissions |
|
|
323
334
|
| OAuth flow | PKCE (Proof Key for Code Exchange) — no client secret needed |
|
|
324
335
|
| Token transmission | OAuth tokens never leave localhost. Only forwarded to `api.anthropic.com` over HTTPS |
|
|
325
336
|
| Network exposure | Proxy binds to `127.0.0.1` only — not accessible from other machines |
|
|
326
|
-
| SSRF protection |
|
|
337
|
+
| SSRF protection | Hardcoded allowlist of API paths — only `/v1/messages`, `/v1/models`, `/v1/complete` are proxied |
|
|
327
338
|
| Token rotation | Refresh tokens rotate on every use (single-use) |
|
|
328
339
|
| Error sanitization | Token patterns redacted from all error messages |
|
|
329
340
|
| Data collection | Zero. No telemetry, no analytics, no phoning home |
|
|
@@ -331,7 +342,7 @@ curl http://localhost:3456/health
|
|
|
331
342
|
## FAQ
|
|
332
343
|
|
|
333
344
|
**Does this violate Anthropic's terms of service?**
|
|
334
|
-
Dario uses
|
|
345
|
+
Dario uses your existing Claude Code credentials with the same OAuth tokens. It authenticates you as you, with your subscription, through Anthropic's official API. The `--cli` mode literally uses Claude Code itself as the backend.
|
|
335
346
|
|
|
336
347
|
**What subscription plans work?**
|
|
337
348
|
Claude Max and Claude Pro. Any plan that lets you use Claude Code.
|
|
@@ -339,17 +350,20 @@ Claude Max and Claude Pro. Any plan that lets you use Claude Code.
|
|
|
339
350
|
**Does it work with Claude Team / Enterprise?**
|
|
340
351
|
Should work if your plan includes Claude Code access. Not tested yet — please open an issue with results.
|
|
341
352
|
|
|
353
|
+
**Do I need Claude Code installed?**
|
|
354
|
+
Recommended but not required. If Claude Code is installed and logged in, `dario login` picks up your credentials automatically. Without Claude Code, dario runs its own OAuth flow to authenticate directly. Note: `--cli` mode requires Claude Code (`npm install -g @anthropic-ai/claude-code`).
|
|
355
|
+
|
|
342
356
|
**What happens when my token expires?**
|
|
343
|
-
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
|
|
357
|
+
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.
|
|
344
358
|
|
|
345
359
|
**I'm getting rate limited on Opus. What do I do?**
|
|
346
360
|
Use `--cli` mode: `dario proxy --cli`. This routes through the Claude Code binary, which continues working when direct API calls are rate limited. You can also enable [extra usage](https://support.claude.com/en/articles/12429409-manage-extra-usage-for-paid-claude-plans) in your Anthropic account settings to extend your limits at API rates.
|
|
347
361
|
|
|
348
362
|
**What are the usage limits?**
|
|
349
|
-
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.
|
|
363
|
+
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. In Claude Code, use `/usage` to check your current limits, or configure the [statusline](https://code.claude.com/docs/en/statusline) to show real-time 5h and 7d utilization percentages.
|
|
350
364
|
|
|
351
365
|
**Can I run this on a server?**
|
|
352
|
-
Dario binds to localhost by default. For server use, you'd need to handle the initial
|
|
366
|
+
Dario binds to localhost by default. For server use, you'd need to 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.
|
|
353
367
|
|
|
354
368
|
**Why "dario"?**
|
|
355
369
|
Named after [Dario Amodei](https://en.wikipedia.org/wiki/Dario_Amodei), CEO of Anthropic.
|
|
@@ -375,13 +389,39 @@ const status = await getStatus();
|
|
|
375
389
|
console.log(status.expiresIn); // "11h 42m"
|
|
376
390
|
```
|
|
377
391
|
|
|
392
|
+
## Trust & Transparency
|
|
393
|
+
|
|
394
|
+
Dario handles your OAuth tokens. Here's why you can trust it:
|
|
395
|
+
|
|
396
|
+
| Signal | Status |
|
|
397
|
+
|--------|--------|
|
|
398
|
+
| **Source code** | ~1000 lines of TypeScript — small enough to read in one sitting |
|
|
399
|
+
| **Dependencies** | 1 production dep (`@anthropic-ai/sdk`). Verify: `npm ls --production` |
|
|
400
|
+
| **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
|
|
401
|
+
| **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
|
|
402
|
+
| **Credential handling** | Tokens never logged, redacted from errors, stored with 0600 permissions |
|
|
403
|
+
| **Network scope** | Binds to 127.0.0.1 only. Upstream traffic goes exclusively to `api.anthropic.com` over HTTPS |
|
|
404
|
+
| **No telemetry** | Zero analytics, tracking, or data collection of any kind |
|
|
405
|
+
| **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
|
|
406
|
+
| **Branch protection** | CI must pass before merge. CODEOWNERS enforces review |
|
|
407
|
+
|
|
408
|
+
Verify the npm package matches this repo:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
# Check provenance attestation
|
|
412
|
+
npm audit signatures 2>/dev/null; npm view @askalf/dario dist.integrity
|
|
413
|
+
|
|
414
|
+
# Check dependency tree (should be minimal)
|
|
415
|
+
cd $(npm root -g)/@askalf/dario && npm ls --production
|
|
416
|
+
```
|
|
417
|
+
|
|
378
418
|
## Contributing
|
|
379
419
|
|
|
380
|
-
PRs welcome. The codebase is ~
|
|
420
|
+
PRs welcome. The codebase is ~1000 lines of TypeScript across 4 files:
|
|
381
421
|
|
|
382
422
|
| File | Purpose |
|
|
383
423
|
|------|---------|
|
|
384
|
-
| `src/oauth.ts` |
|
|
424
|
+
| `src/oauth.ts` | Token storage, refresh logic, Claude Code credential detection, auto OAuth flow |
|
|
385
425
|
| `src/proxy.ts` | HTTP proxy server + CLI backend |
|
|
386
426
|
| `src/cli.ts` | CLI entry point |
|
|
387
427
|
| `src/index.ts` | Library exports |
|
package/dist/cli.js
CHANGED
|
@@ -9,60 +9,39 @@
|
|
|
9
9
|
* dario refresh — Force token refresh
|
|
10
10
|
* dario logout — Remove saved credentials
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { unlink } from 'node:fs/promises';
|
|
12
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
import { homedir } from 'node:os';
|
|
16
|
-
import {
|
|
15
|
+
import { startAutoOAuthFlow, getStatus, refreshTokens } from './oauth.js';
|
|
17
16
|
import { startProxy } from './proxy.js';
|
|
18
17
|
const args = process.argv.slice(2);
|
|
19
18
|
const command = args[0] ?? (process.stdin.isTTY ? 'proxy' : 'proxy');
|
|
20
|
-
function ask(question) {
|
|
21
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
-
return new Promise(resolve => {
|
|
23
|
-
rl.question(question, answer => {
|
|
24
|
-
rl.close();
|
|
25
|
-
resolve(answer.trim());
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
19
|
async function login() {
|
|
30
20
|
console.log('');
|
|
31
|
-
console.log(' dario — Claude
|
|
32
|
-
console.log('
|
|
33
|
-
console.log('');
|
|
34
|
-
const { authUrl, codeVerifier } = startOAuthFlow();
|
|
35
|
-
console.log(' Step 1: Open this URL in your browser:');
|
|
36
|
-
console.log('');
|
|
37
|
-
console.log(` ${authUrl}`);
|
|
21
|
+
console.log(' dario — Claude Login');
|
|
22
|
+
console.log(' ───────────────────');
|
|
38
23
|
console.log('');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(' Step 3: After authorization, you\'ll be redirected to a page');
|
|
42
|
-
console.log(' that shows a code. Copy the FULL URL from your browser\'s');
|
|
43
|
-
console.log(' address bar (it contains the authorization code).');
|
|
44
|
-
console.log('');
|
|
45
|
-
const input = await ask(' Paste the redirect URL or authorization code: ');
|
|
46
|
-
// Extract code from URL or use raw input
|
|
47
|
-
let code = input;
|
|
24
|
+
// Check if Claude Code credentials exist
|
|
25
|
+
const ccPath = join(homedir(), '.claude', '.credentials.json');
|
|
48
26
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
27
|
+
const raw = await readFile(ccPath, 'utf-8');
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
30
|
+
const expiresAt = parsed.claudeAiOauth.expiresAt;
|
|
31
|
+
if (expiresAt > Date.now()) {
|
|
32
|
+
console.log(' Found Claude Code credentials. Starting proxy...');
|
|
33
|
+
console.log('');
|
|
34
|
+
await proxy();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
53
37
|
}
|
|
54
38
|
}
|
|
55
|
-
catch {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!code || code.length < 10 || code.length > 2048) {
|
|
59
|
-
console.error(' Invalid authorization code.');
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
39
|
+
catch { /* no Claude Code credentials, fall through to OAuth */ }
|
|
40
|
+
console.log(' No Claude Code credentials found. Starting OAuth flow...');
|
|
41
|
+
console.log('');
|
|
62
42
|
try {
|
|
63
|
-
const tokens = await
|
|
43
|
+
const tokens = await startAutoOAuthFlow();
|
|
64
44
|
const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 60000);
|
|
65
|
-
console.log('');
|
|
66
45
|
console.log(' Login successful!');
|
|
67
46
|
console.log(` Token expires in ${expiresIn} minutes (auto-refreshes).`);
|
|
68
47
|
console.log('');
|
|
@@ -142,7 +121,7 @@ async function help() {
|
|
|
142
121
|
dario — Use your Claude subscription as an API.
|
|
143
122
|
|
|
144
123
|
Usage:
|
|
145
|
-
dario login
|
|
124
|
+
dario login Detect credentials + start proxy (or run OAuth)
|
|
146
125
|
dario proxy [options] Start the API proxy server
|
|
147
126
|
dario status Check authentication status
|
|
148
127
|
dario refresh Force token refresh
|
|
@@ -151,27 +130,25 @@ async function help() {
|
|
|
151
130
|
Proxy options:
|
|
152
131
|
--model=MODEL Force a model for all requests
|
|
153
132
|
Shortcuts: opus, sonnet, haiku
|
|
133
|
+
Full IDs: claude-opus-4-6, claude-sonnet-4-6
|
|
154
134
|
Default: passthrough (client decides)
|
|
155
135
|
--cli Use Claude CLI as backend (bypasses rate limits)
|
|
156
136
|
--port=PORT Port to listen on (default: 3456)
|
|
157
137
|
--verbose, -v Log all requests
|
|
158
138
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
3. Point any Anthropic SDK at http://localhost:3456
|
|
163
|
-
|
|
164
|
-
Example with OpenClaw, or any tool that uses the Anthropic API:
|
|
165
|
-
ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario openclaw start
|
|
139
|
+
Quick start:
|
|
140
|
+
dario login # auto-detects Claude Code credentials
|
|
141
|
+
dario proxy # or: dario proxy --cli --model=opus
|
|
166
142
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
143
|
+
Then point any Anthropic SDK at http://localhost:3456:
|
|
144
|
+
export ANTHROPIC_BASE_URL=http://localhost:3456
|
|
145
|
+
export ANTHROPIC_API_KEY=dario
|
|
170
146
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
147
|
+
Examples:
|
|
148
|
+
curl http://localhost:3456/v1/messages \\
|
|
149
|
+
-H "Content-Type: application/json" \\
|
|
150
|
+
-H "anthropic-version: 2023-06-01" \\
|
|
151
|
+
-d '{"model":"claude-opus-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
|
|
175
152
|
|
|
176
153
|
Your subscription handles the billing. No API key needed.
|
|
177
154
|
Tokens auto-refresh in the background — set it and forget it.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Use this if you want to embed dario in your own app
|
|
5
5
|
* instead of running the CLI.
|
|
6
6
|
*/
|
|
7
|
-
export {
|
|
7
|
+
export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
|
|
8
8
|
export type { OAuthTokens, CredentialsFile } from './oauth.js';
|
|
9
9
|
export { startProxy } from './proxy.js';
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Use this if you want to embed dario in your own app
|
|
5
5
|
* instead of running the CLI.
|
|
6
6
|
*/
|
|
7
|
-
export {
|
|
7
|
+
export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
|
|
8
8
|
export { startProxy } from './proxy.js';
|
package/dist/oauth.d.ts
CHANGED
|
@@ -15,18 +15,10 @@ export interface CredentialsFile {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function loadCredentials(): Promise<CredentialsFile | null>;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Automatic OAuth flow using a local callback server (same as Claude Code).
|
|
19
|
+
* Opens browser, captures the authorization code automatically.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
22
|
-
authUrl: string;
|
|
23
|
-
state: string;
|
|
24
|
-
codeVerifier: string;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Exchange authorization code for tokens and save them.
|
|
28
|
-
*/
|
|
29
|
-
export declare function exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokens>;
|
|
21
|
+
export declare function startAutoOAuthFlow(): Promise<OAuthTokens>;
|
|
30
22
|
/**
|
|
31
23
|
* Refresh the access token using the refresh token.
|
|
32
24
|
* Retries with exponential backoff on transient failures.
|
package/dist/oauth.js
CHANGED
|
@@ -8,11 +8,11 @@ import { randomBytes, createHash } from 'node:crypto';
|
|
|
8
8
|
import { readFile, writeFile, mkdir, chmod, rename } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
// Claude
|
|
11
|
+
// Claude Code's public OAuth client (PKCE, no secret needed)
|
|
12
12
|
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
13
|
-
const OAUTH_AUTHORIZE_URL = 'https://claude.
|
|
13
|
+
const OAUTH_AUTHORIZE_URL = 'https://platform.claude.com/oauth/authorize';
|
|
14
14
|
const OAUTH_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
15
|
-
const
|
|
15
|
+
const OAUTH_SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
|
|
16
16
|
// Refresh 30 min before expiry
|
|
17
17
|
const REFRESH_BUFFER_MS = 30 * 60 * 1000;
|
|
18
18
|
// In-memory credential cache — avoids disk reads on every request
|
|
@@ -29,31 +29,34 @@ function generatePKCE() {
|
|
|
29
29
|
const codeChallenge = base64url(createHash('sha256').update(codeVerifier).digest());
|
|
30
30
|
return { codeVerifier, codeChallenge };
|
|
31
31
|
}
|
|
32
|
-
function
|
|
32
|
+
function getDarioCredentialsPath() {
|
|
33
33
|
return join(homedir(), '.dario', 'credentials.json');
|
|
34
34
|
}
|
|
35
|
+
function getClaudeCodeCredentialsPath() {
|
|
36
|
+
return join(homedir(), '.claude', '.credentials.json');
|
|
37
|
+
}
|
|
35
38
|
export async function loadCredentials() {
|
|
36
39
|
// Return cached if fresh
|
|
37
40
|
if (credentialsCache && Date.now() - credentialsCacheTime < CACHE_TTL_MS) {
|
|
38
41
|
return credentialsCache;
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// Try dario's own credentials first, then fall back to Claude Code's
|
|
44
|
+
for (const path of [getDarioCredentialsPath(), getClaudeCodeCredentialsPath()]) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(path, 'utf-8');
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
49
|
+
credentialsCache = parsed;
|
|
50
|
+
credentialsCacheTime = Date.now();
|
|
51
|
+
return credentialsCache;
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
credentialsCacheTime = Date.now();
|
|
49
|
-
return credentialsCache;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
54
|
+
catch { /* try next */ }
|
|
53
55
|
}
|
|
56
|
+
return null;
|
|
54
57
|
}
|
|
55
58
|
async function saveCredentials(creds) {
|
|
56
|
-
const path =
|
|
59
|
+
const path = getDarioCredentialsPath();
|
|
57
60
|
await mkdir(dirname(path), { recursive: true });
|
|
58
61
|
// Write atomically: write to temp file, then rename
|
|
59
62
|
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
@@ -69,45 +72,101 @@ async function saveCredentials(creds) {
|
|
|
69
72
|
credentialsCacheTime = Date.now();
|
|
70
73
|
}
|
|
71
74
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
75
|
+
* Automatic OAuth flow using a local callback server (same as Claude Code).
|
|
76
|
+
* Opens browser, captures the authorization code automatically.
|
|
74
77
|
*/
|
|
75
|
-
export function
|
|
78
|
+
export async function startAutoOAuthFlow() {
|
|
79
|
+
const { createServer } = await import('node:http');
|
|
76
80
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
77
81
|
const state = base64url(randomBytes(16));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const server = createServer((req, res) => {
|
|
84
|
+
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
85
|
+
if (url.pathname !== '/callback') {
|
|
86
|
+
res.writeHead(404);
|
|
87
|
+
res.end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const code = url.searchParams.get('code');
|
|
91
|
+
const returnedState = url.searchParams.get('state');
|
|
92
|
+
if (!code) {
|
|
93
|
+
res.writeHead(400);
|
|
94
|
+
res.end('No authorization code received');
|
|
95
|
+
server.close();
|
|
96
|
+
reject(new Error('No authorization code received'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (returnedState !== state) {
|
|
100
|
+
res.writeHead(400);
|
|
101
|
+
res.end('Invalid state parameter');
|
|
102
|
+
server.close();
|
|
103
|
+
reject(new Error('Invalid state parameter'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Redirect browser to success page
|
|
107
|
+
res.writeHead(302, { Location: 'https://platform.claude.com/oauth/code/success?app=claude-code' });
|
|
108
|
+
res.end();
|
|
109
|
+
// Exchange the code for tokens
|
|
110
|
+
server.close();
|
|
111
|
+
exchangeCodeWithRedirect(code, codeVerifier, state, port)
|
|
112
|
+
.then(resolve)
|
|
113
|
+
.catch(reject);
|
|
114
|
+
});
|
|
115
|
+
let port = 0;
|
|
116
|
+
server.listen(0, 'localhost', () => {
|
|
117
|
+
const addr = server.address();
|
|
118
|
+
port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
119
|
+
const params = new URLSearchParams({
|
|
120
|
+
code: 'true',
|
|
121
|
+
client_id: OAUTH_CLIENT_ID,
|
|
122
|
+
response_type: 'code',
|
|
123
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
124
|
+
scope: OAUTH_SCOPES,
|
|
125
|
+
code_challenge: codeChallenge,
|
|
126
|
+
code_challenge_method: 'S256',
|
|
127
|
+
state,
|
|
128
|
+
});
|
|
129
|
+
const authUrl = `${OAUTH_AUTHORIZE_URL}?${params.toString()}`;
|
|
130
|
+
// Open browser
|
|
131
|
+
console.log(' Opening browser to sign in...');
|
|
132
|
+
console.log(` If the browser didn't open, visit: ${authUrl}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
// Open browser using platform-specific commands (no external deps)
|
|
135
|
+
const { exec } = require('node:child_process');
|
|
136
|
+
const cmd = process.platform === 'win32' ? `start "" "${authUrl}"`
|
|
137
|
+
: process.platform === 'darwin' ? `open "${authUrl}"`
|
|
138
|
+
: `xdg-open "${authUrl}"`;
|
|
139
|
+
exec(cmd, () => { });
|
|
140
|
+
});
|
|
141
|
+
server.on('error', (err) => {
|
|
142
|
+
reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
|
|
143
|
+
});
|
|
144
|
+
// Timeout after 5 minutes
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
server.close();
|
|
147
|
+
reject(new Error('OAuth flow timed out. Try again with `dario login`.'));
|
|
148
|
+
}, 300_000);
|
|
86
149
|
});
|
|
87
|
-
return {
|
|
88
|
-
authUrl: `${OAUTH_AUTHORIZE_URL}?${params.toString()}`,
|
|
89
|
-
state,
|
|
90
|
-
codeVerifier,
|
|
91
|
-
};
|
|
92
150
|
}
|
|
93
151
|
/**
|
|
94
|
-
* Exchange
|
|
152
|
+
* Exchange code using the localhost redirect URI.
|
|
95
153
|
*/
|
|
96
|
-
|
|
154
|
+
async function exchangeCodeWithRedirect(code, codeVerifier, state, port) {
|
|
97
155
|
const res = await fetch(OAUTH_TOKEN_URL, {
|
|
98
156
|
method: 'POST',
|
|
99
|
-
headers: { 'Content-Type': 'application/
|
|
100
|
-
body:
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({
|
|
101
159
|
grant_type: 'authorization_code',
|
|
102
160
|
client_id: OAUTH_CLIENT_ID,
|
|
103
161
|
code,
|
|
104
|
-
redirect_uri:
|
|
162
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
105
163
|
code_verifier: codeVerifier,
|
|
164
|
+
state,
|
|
106
165
|
}),
|
|
107
166
|
signal: AbortSignal.timeout(30000),
|
|
108
167
|
});
|
|
109
168
|
if (!res.ok) {
|
|
110
|
-
throw new Error(`Token exchange failed (${res.status}).
|
|
169
|
+
throw new Error(`Token exchange failed (${res.status}). Try again with \`dario login\`.`);
|
|
111
170
|
}
|
|
112
171
|
const data = await res.json();
|
|
113
172
|
const tokens = {
|
package/dist/proxy.js
CHANGED
|
@@ -176,8 +176,10 @@ export async function startProxy(opts = {}) {
|
|
|
176
176
|
res.end();
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
|
+
// Strip query parameters for endpoint matching
|
|
180
|
+
const urlPath = req.url?.split('?')[0] ?? '';
|
|
179
181
|
// Health check
|
|
180
|
-
if (
|
|
182
|
+
if (urlPath === '/health' || urlPath === '/') {
|
|
181
183
|
const s = await getStatus();
|
|
182
184
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
183
185
|
res.end(JSON.stringify({
|
|
@@ -189,20 +191,19 @@ export async function startProxy(opts = {}) {
|
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
// Status endpoint
|
|
192
|
-
if (
|
|
194
|
+
if (urlPath === '/status') {
|
|
193
195
|
const s = await getStatus();
|
|
194
196
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
195
197
|
res.end(JSON.stringify(s));
|
|
196
198
|
return;
|
|
197
199
|
}
|
|
198
200
|
// Allowlisted API paths — only these are proxied (prevents SSRF)
|
|
199
|
-
const rawPath = req.url?.split('?')[0] ?? '';
|
|
200
201
|
const allowedPaths = {
|
|
201
202
|
'/v1/messages': `${ANTHROPIC_API}/v1/messages`,
|
|
202
203
|
'/v1/models': `${ANTHROPIC_API}/v1/models`,
|
|
203
204
|
'/v1/complete': `${ANTHROPIC_API}/v1/complete`,
|
|
204
205
|
};
|
|
205
|
-
const targetBase = allowedPaths[
|
|
206
|
+
const targetBase = allowedPaths[urlPath];
|
|
206
207
|
if (!targetBase) {
|
|
207
208
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
208
209
|
res.end(JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' }));
|
|
@@ -232,7 +233,7 @@ export async function startProxy(opts = {}) {
|
|
|
232
233
|
}
|
|
233
234
|
const body = Buffer.concat(chunks);
|
|
234
235
|
// CLI backend mode: route through claude --print
|
|
235
|
-
if (useCli &&
|
|
236
|
+
if (useCli && urlPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
|
|
236
237
|
const cliResult = await handleViaCli(body, modelOverride, verbose);
|
|
237
238
|
requestCount++;
|
|
238
239
|
res.writeHead(cliResult.status, {
|
|
@@ -265,6 +266,7 @@ export async function startProxy(opts = {}) {
|
|
|
265
266
|
'interleaved-thinking-2025-05-14',
|
|
266
267
|
'prompt-caching-scope-2026-01-05',
|
|
267
268
|
'claude-code-20250219',
|
|
269
|
+
'context-management-2025-06-27',
|
|
268
270
|
]);
|
|
269
271
|
if (clientBeta) {
|
|
270
272
|
for (const flag of clientBeta.split(',')) {
|
|
@@ -280,6 +282,7 @@ export async function startProxy(opts = {}) {
|
|
|
280
282
|
'anthropic-version': req.headers['anthropic-version'] || '2023-06-01',
|
|
281
283
|
'anthropic-beta': [...betaFlags].join(','),
|
|
282
284
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
285
|
+
'anthropic-client-platform': 'cli',
|
|
283
286
|
'user-agent': `claude-cli/${cliVersion} (external, cli)`,
|
|
284
287
|
'x-app': 'cli',
|
|
285
288
|
'x-claude-code-session-id': SESSION_ID,
|
|
@@ -307,11 +310,11 @@ export async function startProxy(opts = {}) {
|
|
|
307
310
|
'Content-Type': contentType || 'application/json',
|
|
308
311
|
'Access-Control-Allow-Origin': CORS_ORIGIN,
|
|
309
312
|
};
|
|
310
|
-
// Forward rate limit headers
|
|
311
|
-
for (const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
// Forward rate limit headers (including unified subscription headers)
|
|
314
|
+
for (const [key, value] of upstream.headers.entries()) {
|
|
315
|
+
if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
|
|
316
|
+
responseHeaders[key] = value;
|
|
317
|
+
}
|
|
315
318
|
}
|
|
316
319
|
requestCount++;
|
|
317
320
|
res.writeHead(upstream.status, responseHeaders);
|
|
@@ -382,6 +385,33 @@ export async function startProxy(opts = {}) {
|
|
|
382
385
|
console.log(` ${modelLine}`);
|
|
383
386
|
console.log('');
|
|
384
387
|
});
|
|
388
|
+
// Session presence heartbeat — registers this proxy as an active Claude Code session
|
|
389
|
+
// Claude Code sends this every 5 seconds; the server uses it for priority routing
|
|
390
|
+
const clientId = randomUUID();
|
|
391
|
+
const connectedAt = new Date().toISOString();
|
|
392
|
+
let lastPresencePulse = 0;
|
|
393
|
+
const presenceInterval = setInterval(async () => {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
if (now - lastPresencePulse < 5000)
|
|
396
|
+
return;
|
|
397
|
+
lastPresencePulse = now;
|
|
398
|
+
try {
|
|
399
|
+
const token = await getAccessToken();
|
|
400
|
+
const presenceUrl = `${ANTHROPIC_API}/v1/code/sessions/${SESSION_ID}/client/presence`;
|
|
401
|
+
await fetch(presenceUrl, {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
headers: {
|
|
404
|
+
'Authorization': `Bearer ${token}`,
|
|
405
|
+
'Content-Type': 'application/json',
|
|
406
|
+
'anthropic-version': '2023-06-01',
|
|
407
|
+
'anthropic-client-platform': 'cli',
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify({ client_id: clientId, connected_at: connectedAt }),
|
|
410
|
+
signal: AbortSignal.timeout(5000),
|
|
411
|
+
}).catch(() => { });
|
|
412
|
+
}
|
|
413
|
+
catch { /* presence is best-effort */ }
|
|
414
|
+
}, 5000);
|
|
385
415
|
// Periodic token refresh (every 15 minutes)
|
|
386
416
|
const refreshInterval = setInterval(async () => {
|
|
387
417
|
try {
|
|
@@ -398,6 +428,7 @@ export async function startProxy(opts = {}) {
|
|
|
398
428
|
// Graceful shutdown
|
|
399
429
|
const shutdown = () => {
|
|
400
430
|
console.log('\n[dario] Shutting down...');
|
|
431
|
+
clearInterval(presenceInterval);
|
|
401
432
|
clearInterval(refreshInterval);
|
|
402
433
|
server.close(() => process.exit(0));
|
|
403
434
|
// Force exit after 5s if connections don't close
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "Use your Claude subscription as an API.
|
|
3
|
+
"version": "1.2.1",
|
|
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": {
|
|
7
7
|
"dario": "./dist/cli.js"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
|
+
"audit": "npm audit --production --audit-level=high",
|
|
24
25
|
"prepublishOnly": "npm run build",
|
|
25
26
|
"start": "node dist/cli.js",
|
|
26
27
|
"dev": "tsx src/cli.ts"
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"cli",
|
|
41
42
|
"developer-tools"
|
|
42
43
|
],
|
|
43
|
-
"author": "askalf",
|
|
44
|
+
"author": "askalf (https://github.com/askalf)",
|
|
44
45
|
"license": "MIT",
|
|
45
46
|
"repository": {
|
|
46
47
|
"type": "git",
|