@askalf/dario 2.1.2 → 2.2.0
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 +16 -0
- package/dist/proxy.js +60 -22
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -154,6 +154,19 @@ async function help() {
|
|
|
154
154
|
Tokens auto-refresh in the background — set it and forget it.
|
|
155
155
|
`);
|
|
156
156
|
}
|
|
157
|
+
async function version() {
|
|
158
|
+
const { readFile } = await import('node:fs/promises');
|
|
159
|
+
const { fileURLToPath } = await import('node:url');
|
|
160
|
+
const { dirname, join } = await import('node:path');
|
|
161
|
+
try {
|
|
162
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
163
|
+
const pkg = JSON.parse(await readFile(join(dir, '..', 'package.json'), 'utf-8'));
|
|
164
|
+
console.log(pkg.version);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
console.log('unknown');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
157
170
|
// Main
|
|
158
171
|
const commands = {
|
|
159
172
|
login,
|
|
@@ -162,8 +175,11 @@ const commands = {
|
|
|
162
175
|
refresh,
|
|
163
176
|
logout,
|
|
164
177
|
help,
|
|
178
|
+
version,
|
|
165
179
|
'--help': help,
|
|
166
180
|
'-h': help,
|
|
181
|
+
'--version': version,
|
|
182
|
+
'-V': version,
|
|
167
183
|
};
|
|
168
184
|
const handler = commands[command];
|
|
169
185
|
if (!handler) {
|
package/dist/proxy.js
CHANGED
|
@@ -7,6 +7,7 @@ const ANTHROPIC_API = 'https://api.anthropic.com';
|
|
|
7
7
|
const DEFAULT_PORT = 3456;
|
|
8
8
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
|
|
9
9
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
10
|
+
const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
|
|
10
11
|
const LOCALHOST = '127.0.0.1';
|
|
11
12
|
// Detect installed Claude Code version at startup
|
|
12
13
|
function detectClaudeVersion() {
|
|
@@ -101,7 +102,7 @@ export function sanitizeError(err) {
|
|
|
101
102
|
return msg
|
|
102
103
|
.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]')
|
|
103
104
|
.replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
|
|
104
|
-
.replace(/Bearer\s+[
|
|
105
|
+
.replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]');
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
108
|
* CLI Backend: route requests through `claude --print` instead of direct API.
|
|
@@ -146,8 +147,11 @@ async function handleViaCli(body, model, verbose) {
|
|
|
146
147
|
});
|
|
147
148
|
let stdout = '';
|
|
148
149
|
let stderr = '';
|
|
149
|
-
|
|
150
|
-
child.
|
|
150
|
+
const MAX_CLI_OUTPUT = 5_000_000; // 5MB cap per stream — prevents OOM from runaway CLI
|
|
151
|
+
child.stdout.on('data', (d) => { if (stdout.length < MAX_CLI_OUTPUT)
|
|
152
|
+
stdout += d.toString(); });
|
|
153
|
+
child.stderr.on('data', (d) => { if (stderr.length < MAX_CLI_OUTPUT)
|
|
154
|
+
stderr += d.toString(); });
|
|
151
155
|
child.stdin.write(prompt);
|
|
152
156
|
child.stdin.end();
|
|
153
157
|
child.on('close', (code) => {
|
|
@@ -229,13 +233,21 @@ export async function startProxy(opts = {}) {
|
|
|
229
233
|
const apiKey = process.env.DARIO_API_KEY;
|
|
230
234
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
231
235
|
const corsOrigin = `http://localhost:${port}`;
|
|
236
|
+
// Security headers for all responses
|
|
237
|
+
const SECURITY_HEADERS = {
|
|
238
|
+
'X-Content-Type-Options': 'nosniff',
|
|
239
|
+
'X-Frame-Options': 'DENY',
|
|
240
|
+
'Cache-Control': 'no-store',
|
|
241
|
+
};
|
|
232
242
|
// Pre-serialize static responses
|
|
233
243
|
const CORS_HEADERS = {
|
|
234
244
|
'Access-Control-Allow-Origin': corsOrigin,
|
|
235
245
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
236
246
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
|
|
237
247
|
'Access-Control-Max-Age': '86400',
|
|
248
|
+
...SECURITY_HEADERS,
|
|
238
249
|
};
|
|
250
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json', ...SECURITY_HEADERS };
|
|
239
251
|
const MODELS_JSON = JSON.stringify(OPENAI_MODELS_LIST);
|
|
240
252
|
const ERR_UNAUTH = JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' });
|
|
241
253
|
const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' });
|
|
@@ -263,7 +275,7 @@ export async function startProxy(opts = {}) {
|
|
|
263
275
|
// Health check
|
|
264
276
|
if (urlPath === '/health' || urlPath === '/') {
|
|
265
277
|
const s = await getStatus();
|
|
266
|
-
res.writeHead(200,
|
|
278
|
+
res.writeHead(200, JSON_HEADERS);
|
|
267
279
|
res.end(JSON.stringify({
|
|
268
280
|
status: 'ok',
|
|
269
281
|
oauth: s.status,
|
|
@@ -273,20 +285,20 @@ export async function startProxy(opts = {}) {
|
|
|
273
285
|
return;
|
|
274
286
|
}
|
|
275
287
|
if (!checkAuth(req)) {
|
|
276
|
-
res.writeHead(401,
|
|
288
|
+
res.writeHead(401, JSON_HEADERS);
|
|
277
289
|
res.end(ERR_UNAUTH);
|
|
278
290
|
return;
|
|
279
291
|
}
|
|
280
292
|
// Status endpoint
|
|
281
293
|
if (urlPath === '/status') {
|
|
282
294
|
const s = await getStatus();
|
|
283
|
-
res.writeHead(200,
|
|
295
|
+
res.writeHead(200, JSON_HEADERS);
|
|
284
296
|
res.end(JSON.stringify(s));
|
|
285
297
|
return;
|
|
286
298
|
}
|
|
287
299
|
if (urlPath === '/v1/models' && req.method === 'GET') {
|
|
288
300
|
requestCount++;
|
|
289
|
-
res.writeHead(200, {
|
|
301
|
+
res.writeHead(200, { ...JSON_HEADERS, 'Access-Control-Allow-Origin': corsOrigin });
|
|
290
302
|
res.end(MODELS_JSON);
|
|
291
303
|
return;
|
|
292
304
|
}
|
|
@@ -299,39 +311,64 @@ export async function startProxy(opts = {}) {
|
|
|
299
311
|
};
|
|
300
312
|
const targetBase = isOpenAI ? `${ANTHROPIC_API}/v1/messages` : allowedPaths[urlPath];
|
|
301
313
|
if (!targetBase) {
|
|
302
|
-
res.writeHead(403,
|
|
314
|
+
res.writeHead(403, JSON_HEADERS);
|
|
303
315
|
res.end(ERR_FORBIDDEN);
|
|
304
316
|
return;
|
|
305
317
|
}
|
|
306
318
|
if (req.method !== 'POST') {
|
|
307
|
-
res.writeHead(405,
|
|
319
|
+
res.writeHead(405, JSON_HEADERS);
|
|
308
320
|
res.end(ERR_METHOD);
|
|
309
321
|
return;
|
|
310
322
|
}
|
|
311
323
|
// Proxy to Anthropic
|
|
312
324
|
try {
|
|
313
325
|
const accessToken = await getAccessToken();
|
|
314
|
-
// Read request body with size limit
|
|
326
|
+
// Read request body with size limit and timeout (prevents slow-loris)
|
|
315
327
|
const chunks = [];
|
|
316
328
|
let totalBytes = 0;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
const bodyTimeout = setTimeout(() => { req.destroy(); }, BODY_READ_TIMEOUT_MS);
|
|
330
|
+
try {
|
|
331
|
+
for await (const chunk of req) {
|
|
332
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
333
|
+
totalBytes += buf.length;
|
|
334
|
+
if (totalBytes > MAX_BODY_BYTES) {
|
|
335
|
+
clearTimeout(bodyTimeout);
|
|
336
|
+
res.writeHead(413, JSON_HEADERS);
|
|
337
|
+
res.end(JSON.stringify({ error: 'Request body too large', max: `${MAX_BODY_BYTES / 1024 / 1024}MB` }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
chunks.push(buf);
|
|
324
341
|
}
|
|
325
|
-
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
clearTimeout(bodyTimeout);
|
|
326
345
|
}
|
|
327
346
|
const body = Buffer.concat(chunks);
|
|
328
|
-
// CLI backend mode: route through claude --print
|
|
329
|
-
if (useCli &&
|
|
330
|
-
|
|
347
|
+
// CLI backend mode: route through claude --print (works for both Anthropic and OpenAI endpoints)
|
|
348
|
+
if (useCli && req.method === 'POST' && body.length > 0) {
|
|
349
|
+
let cliBody = body;
|
|
350
|
+
// Translate OpenAI format before passing to CLI
|
|
351
|
+
if (isOpenAI) {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(body.toString());
|
|
354
|
+
cliBody = Buffer.from(JSON.stringify(openaiToAnthropic(parsed, modelOverride)));
|
|
355
|
+
}
|
|
356
|
+
catch { /* send as-is */ }
|
|
357
|
+
}
|
|
358
|
+
const cliResult = await handleViaCli(cliBody, modelOverride, verbose);
|
|
331
359
|
requestCount++;
|
|
360
|
+
// Translate CLI response back to OpenAI format if needed
|
|
361
|
+
if (isOpenAI && cliResult.status >= 200 && cliResult.status < 300) {
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(cliResult.body);
|
|
364
|
+
cliResult.body = JSON.stringify(anthropicToOpenai(parsed));
|
|
365
|
+
}
|
|
366
|
+
catch { /* send as-is */ }
|
|
367
|
+
}
|
|
332
368
|
res.writeHead(cliResult.status, {
|
|
333
369
|
'Content-Type': cliResult.contentType,
|
|
334
370
|
'Access-Control-Allow-Origin': corsOrigin,
|
|
371
|
+
...SECURITY_HEADERS,
|
|
335
372
|
});
|
|
336
373
|
res.end(cliResult.body);
|
|
337
374
|
return;
|
|
@@ -375,6 +412,7 @@ export async function startProxy(opts = {}) {
|
|
|
375
412
|
const responseHeaders = {
|
|
376
413
|
'Content-Type': contentType || 'application/json',
|
|
377
414
|
'Access-Control-Allow-Origin': corsOrigin,
|
|
415
|
+
...SECURITY_HEADERS,
|
|
378
416
|
};
|
|
379
417
|
// Forward rate limit headers (including unified subscription headers)
|
|
380
418
|
for (const [key, value] of upstream.headers.entries()) {
|
|
@@ -450,7 +488,7 @@ export async function startProxy(opts = {}) {
|
|
|
450
488
|
catch (err) {
|
|
451
489
|
// Log full error server-side, return generic message to client
|
|
452
490
|
console.error('[dario] Proxy error:', sanitizeError(err));
|
|
453
|
-
res.writeHead(502,
|
|
491
|
+
res.writeHead(502, JSON_HEADERS);
|
|
454
492
|
res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
|
|
455
493
|
}
|
|
456
494
|
});
|