@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 +6 -1
- package/dist/oauth.d.ts +1 -0
- package/dist/oauth.js +34 -1
- package/dist/proxy.js +14 -2
- 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
|
@@ -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
|
|
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
|
}
|