@askalf/dario 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6 -1
- package/dist/oauth.d.ts +1 -0
- package/dist/oauth.js +34 -1
- package/dist/proxy.js +58 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -119,6 +119,10 @@ async function logout() {
|
|
|
119
119
|
async function proxy() {
|
|
120
120
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
121
121
|
const port = portArg ? parseInt(portArg.split('=')[1]) : 3456;
|
|
122
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
123
|
+
console.error('[dario] Invalid port. Must be 1-65535.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
122
126
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
123
127
|
await startProxy({ port, verbose });
|
|
124
128
|
}
|
|
@@ -176,6 +180,7 @@ if (!handler) {
|
|
|
176
180
|
process.exit(1);
|
|
177
181
|
}
|
|
178
182
|
handler().catch(err => {
|
|
179
|
-
|
|
183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
184
|
+
console.error('Fatal error:', msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]'));
|
|
180
185
|
process.exit(1);
|
|
181
186
|
});
|
package/dist/oauth.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function exchangeCode(code: string, codeVerifier: string): Promis
|
|
|
30
30
|
/**
|
|
31
31
|
* Refresh the access token using the refresh token.
|
|
32
32
|
* Retries with exponential backoff on transient failures.
|
|
33
|
+
* Uses a mutex to prevent concurrent refresh races.
|
|
33
34
|
*/
|
|
34
35
|
export declare function refreshTokens(): Promise<OAuthTokens>;
|
|
35
36
|
/**
|
package/dist/oauth.js
CHANGED
|
@@ -15,6 +15,12 @@ const OAUTH_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
|
15
15
|
const OAUTH_REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
|
|
16
16
|
// Refresh 30 min before expiry
|
|
17
17
|
const REFRESH_BUFFER_MS = 30 * 60 * 1000;
|
|
18
|
+
// In-memory credential cache — avoids disk reads on every request
|
|
19
|
+
let credentialsCache = null;
|
|
20
|
+
let credentialsCacheTime = 0;
|
|
21
|
+
const CACHE_TTL_MS = 10_000; // Re-read from disk every 10s at most
|
|
22
|
+
// Mutex to prevent concurrent refresh races
|
|
23
|
+
let refreshInProgress = null;
|
|
18
24
|
function base64url(buf) {
|
|
19
25
|
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
20
26
|
}
|
|
@@ -27,9 +33,20 @@ function getCredentialsPath() {
|
|
|
27
33
|
return join(homedir(), '.dario', 'credentials.json');
|
|
28
34
|
}
|
|
29
35
|
export async function loadCredentials() {
|
|
36
|
+
// Return cached if fresh
|
|
37
|
+
if (credentialsCache && Date.now() - credentialsCacheTime < CACHE_TTL_MS) {
|
|
38
|
+
return credentialsCache;
|
|
39
|
+
}
|
|
30
40
|
try {
|
|
31
41
|
const raw = await readFile(getCredentialsPath(), 'utf-8');
|
|
32
|
-
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
// Validate structure
|
|
44
|
+
if (!parsed?.claudeAiOauth?.accessToken || !parsed?.claudeAiOauth?.refreshToken) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
credentialsCache = parsed;
|
|
48
|
+
credentialsCacheTime = Date.now();
|
|
49
|
+
return credentialsCache;
|
|
33
50
|
}
|
|
34
51
|
catch {
|
|
35
52
|
return null;
|
|
@@ -48,6 +65,9 @@ async function saveCredentials(creds) {
|
|
|
48
65
|
await chmod(path, 0o600);
|
|
49
66
|
}
|
|
50
67
|
catch { /* Windows ignores file modes */ }
|
|
68
|
+
// Invalidate cache so next read picks up the new tokens
|
|
69
|
+
credentialsCache = creds;
|
|
70
|
+
credentialsCacheTime = Date.now();
|
|
51
71
|
}
|
|
52
72
|
/**
|
|
53
73
|
* Start the OAuth flow. Returns the authorization URL and PKCE state
|
|
@@ -103,8 +123,21 @@ export async function exchangeCode(code, codeVerifier) {
|
|
|
103
123
|
/**
|
|
104
124
|
* Refresh the access token using the refresh token.
|
|
105
125
|
* Retries with exponential backoff on transient failures.
|
|
126
|
+
* Uses a mutex to prevent concurrent refresh races.
|
|
106
127
|
*/
|
|
107
128
|
export async function refreshTokens() {
|
|
129
|
+
// Prevent concurrent refreshes — if one is already in progress, wait for it
|
|
130
|
+
if (refreshInProgress)
|
|
131
|
+
return refreshInProgress;
|
|
132
|
+
refreshInProgress = doRefreshTokens();
|
|
133
|
+
try {
|
|
134
|
+
return await refreshInProgress;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
refreshInProgress = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function doRefreshTokens() {
|
|
108
141
|
const creds = await loadCredentials();
|
|
109
142
|
if (!creds?.claudeAiOauth?.refreshToken) {
|
|
110
143
|
throw new Error('No refresh token available. Run `dario login` first.');
|
package/dist/proxy.js
CHANGED
|
@@ -8,12 +8,37 @@
|
|
|
8
8
|
* No API key needed — your Claude subscription pays for it.
|
|
9
9
|
*/
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { arch, platform, version as nodeVersion } from 'node:process';
|
|
11
14
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
12
15
|
const ANTHROPIC_API = 'https://api.anthropic.com';
|
|
13
16
|
const DEFAULT_PORT = 3456;
|
|
14
17
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
|
|
18
|
+
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
15
19
|
const LOCALHOST = '127.0.0.1';
|
|
16
20
|
const CORS_ORIGIN = 'http://localhost';
|
|
21
|
+
// Detect installed Claude Code version at startup
|
|
22
|
+
function detectClaudeVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const out = execSync('claude --version', { timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
25
|
+
const match = out.match(/^([\d.]+)/);
|
|
26
|
+
return match?.[1] ?? '2.1.96';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return '2.1.96';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function getOsName() {
|
|
33
|
+
const p = platform;
|
|
34
|
+
if (p === 'win32')
|
|
35
|
+
return 'Windows';
|
|
36
|
+
if (p === 'darwin')
|
|
37
|
+
return 'MacOS';
|
|
38
|
+
return 'Linux';
|
|
39
|
+
}
|
|
40
|
+
// Persistent session ID per proxy lifetime (like Claude Code does per session)
|
|
41
|
+
const SESSION_ID = randomUUID();
|
|
17
42
|
function sanitizeError(err) {
|
|
18
43
|
const msg = err instanceof Error ? err.message : String(err);
|
|
19
44
|
// Never leak tokens in error messages
|
|
@@ -28,6 +53,7 @@ export async function startProxy(opts = {}) {
|
|
|
28
53
|
console.error('[dario] Not authenticated. Run `dario login` first.');
|
|
29
54
|
process.exit(1);
|
|
30
55
|
}
|
|
56
|
+
const cliVersion = detectClaudeVersion();
|
|
31
57
|
let requestCount = 0;
|
|
32
58
|
let tokenCostEstimate = 0;
|
|
33
59
|
const server = createServer(async (req, res) => {
|
|
@@ -105,7 +131,12 @@ export async function startProxy(opts = {}) {
|
|
|
105
131
|
const targetUrl = targetBase;
|
|
106
132
|
// Merge any client-provided beta flags with the required oauth flag
|
|
107
133
|
const clientBeta = req.headers['anthropic-beta'];
|
|
108
|
-
const betaFlags = new Set([
|
|
134
|
+
const betaFlags = new Set([
|
|
135
|
+
'oauth-2025-04-20',
|
|
136
|
+
'interleaved-thinking-2025-05-14',
|
|
137
|
+
'prompt-caching-scope-2026-01-05',
|
|
138
|
+
'claude-code-20250219',
|
|
139
|
+
]);
|
|
109
140
|
if (clientBeta) {
|
|
110
141
|
for (const flag of clientBeta.split(',')) {
|
|
111
142
|
const trimmed = flag.trim();
|
|
@@ -114,16 +145,30 @@ export async function startProxy(opts = {}) {
|
|
|
114
145
|
}
|
|
115
146
|
}
|
|
116
147
|
const headers = {
|
|
148
|
+
'accept': 'application/json',
|
|
117
149
|
'Authorization': `Bearer ${accessToken}`,
|
|
118
150
|
'Content-Type': 'application/json',
|
|
119
151
|
'anthropic-version': req.headers['anthropic-version'] || '2023-06-01',
|
|
120
152
|
'anthropic-beta': [...betaFlags].join(','),
|
|
153
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
154
|
+
'user-agent': `claude-cli/${cliVersion} (external, cli)`,
|
|
121
155
|
'x-app': 'cli',
|
|
156
|
+
'x-claude-code-session-id': SESSION_ID,
|
|
157
|
+
'x-client-request-id': randomUUID(),
|
|
158
|
+
'x-stainless-arch': arch,
|
|
159
|
+
'x-stainless-lang': 'js',
|
|
160
|
+
'x-stainless-os': getOsName(),
|
|
161
|
+
'x-stainless-package-version': '0.81.0',
|
|
162
|
+
'x-stainless-retry-count': '0',
|
|
163
|
+
'x-stainless-runtime': 'node',
|
|
164
|
+
'x-stainless-runtime-version': nodeVersion,
|
|
165
|
+
'x-stainless-timeout': '600',
|
|
122
166
|
};
|
|
123
167
|
const upstream = await fetch(targetUrl, {
|
|
124
168
|
method: req.method ?? 'POST',
|
|
125
169
|
headers,
|
|
126
170
|
body: body.length > 0 ? body : undefined,
|
|
171
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
127
172
|
// @ts-expect-error — duplex needed for streaming
|
|
128
173
|
duplex: 'half',
|
|
129
174
|
});
|
|
@@ -187,7 +232,7 @@ export async function startProxy(opts = {}) {
|
|
|
187
232
|
server.listen(port, LOCALHOST, () => {
|
|
188
233
|
const oauthLine = `OAuth: ${status.status} (expires in ${status.expiresIn})`;
|
|
189
234
|
console.log('');
|
|
190
|
-
console.log(` dario
|
|
235
|
+
console.log(` dario — http://localhost:${port}`);
|
|
191
236
|
console.log('');
|
|
192
237
|
console.log(' Your Claude subscription is now an API.');
|
|
193
238
|
console.log('');
|
|
@@ -199,7 +244,7 @@ export async function startProxy(opts = {}) {
|
|
|
199
244
|
console.log('');
|
|
200
245
|
});
|
|
201
246
|
// Periodic token refresh (every 15 minutes)
|
|
202
|
-
setInterval(async () => {
|
|
247
|
+
const refreshInterval = setInterval(async () => {
|
|
203
248
|
try {
|
|
204
249
|
const s = await getStatus();
|
|
205
250
|
if (s.status === 'expiring') {
|
|
@@ -211,4 +256,14 @@ export async function startProxy(opts = {}) {
|
|
|
211
256
|
console.error('[dario] Background refresh error:', err instanceof Error ? err.message : err);
|
|
212
257
|
}
|
|
213
258
|
}, 15 * 60 * 1000);
|
|
259
|
+
// Graceful shutdown
|
|
260
|
+
const shutdown = () => {
|
|
261
|
+
console.log('\n[dario] Shutting down...');
|
|
262
|
+
clearInterval(refreshInterval);
|
|
263
|
+
server.close(() => process.exit(0));
|
|
264
|
+
// Force exit after 5s if connections don't close
|
|
265
|
+
setTimeout(() => process.exit(0), 5000).unref();
|
|
266
|
+
};
|
|
267
|
+
process.on('SIGINT', shutdown);
|
|
268
|
+
process.on('SIGTERM', shutdown);
|
|
214
269
|
}
|