@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 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
- console.error('Fatal error:', err);
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
- return JSON.parse(raw);
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(['oauth-2025-04-20']);
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 v1.0.0 — http://localhost:${port}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Use your Claude subscription as an API. Two commands, no API key. OAuth bridge for Claude Max/Pro.",
5
5
  "type": "module",
6
6
  "bin": {