@askalf/dario 1.0.5 → 1.0.6

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
@@ -12,6 +12,7 @@ import { getAccessToken, getStatus } from './oauth.js';
12
12
  const ANTHROPIC_API = 'https://api.anthropic.com';
13
13
  const DEFAULT_PORT = 3456;
14
14
  const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
15
+ const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
15
16
  const LOCALHOST = '127.0.0.1';
16
17
  const CORS_ORIGIN = 'http://localhost';
17
18
  function sanitizeError(err) {
@@ -124,6 +125,7 @@ export async function startProxy(opts = {}) {
124
125
  method: req.method ?? 'POST',
125
126
  headers,
126
127
  body: body.length > 0 ? body : undefined,
128
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
127
129
  // @ts-expect-error — duplex needed for streaming
128
130
  duplex: 'half',
129
131
  });
@@ -187,7 +189,7 @@ export async function startProxy(opts = {}) {
187
189
  server.listen(port, LOCALHOST, () => {
188
190
  const oauthLine = `OAuth: ${status.status} (expires in ${status.expiresIn})`;
189
191
  console.log('');
190
- console.log(` dario v1.0.0 — http://localhost:${port}`);
192
+ console.log(` dario — http://localhost:${port}`);
191
193
  console.log('');
192
194
  console.log(' Your Claude subscription is now an API.');
193
195
  console.log('');
@@ -199,7 +201,7 @@ export async function startProxy(opts = {}) {
199
201
  console.log('');
200
202
  });
201
203
  // Periodic token refresh (every 15 minutes)
202
- setInterval(async () => {
204
+ const refreshInterval = setInterval(async () => {
203
205
  try {
204
206
  const s = await getStatus();
205
207
  if (s.status === 'expiring') {
@@ -211,4 +213,14 @@ export async function startProxy(opts = {}) {
211
213
  console.error('[dario] Background refresh error:', err instanceof Error ? err.message : err);
212
214
  }
213
215
  }, 15 * 60 * 1000);
216
+ // Graceful shutdown
217
+ const shutdown = () => {
218
+ console.log('\n[dario] Shutting down...');
219
+ clearInterval(refreshInterval);
220
+ server.close(() => process.exit(0));
221
+ // Force exit after 5s if connections don't close
222
+ setTimeout(() => process.exit(0), 5000).unref();
223
+ };
224
+ process.on('SIGINT', shutdown);
225
+ process.on('SIGTERM', shutdown);
214
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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": {