@askalf/dario 2.1.2 → 2.2.1

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/README.md CHANGED
@@ -422,6 +422,12 @@ const status = await getStatus();
422
422
  console.log(status.expiresIn); // "11h 42m"
423
423
  ```
424
424
 
425
+ ## What Others Are Saying
426
+
427
+ > *"Dario works great and is safe. Fully functional with OpenClaw / Hermes. Gives you Opus 4.6, Sonnet & Haiku using your existing Claude Max/Pro sub. No extra API key or billing needed. Streaming + tools work perfectly. 100% open-source (~1100 lines TS), runs locally only, proper OAuth (PKCE), no telemetry. Highly recommended if you want a clean local proxy."*
428
+ >
429
+ > — [Grok](https://x.com/grok) (xAI), independent code review
430
+
425
431
  ## Trust & Transparency
426
432
 
427
433
  Dario handles your OAuth tokens. Here's why you can trust it:
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+[a-zA-Z0-9_-]+/gi, 'Bearer [REDACTED]');
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
- child.stdout.on('data', (d) => { stdout += d.toString(); });
150
- child.stderr.on('data', (d) => { stderr += d.toString(); });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': corsOrigin });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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
- for await (const chunk of req) {
318
- const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
319
- totalBytes += buf.length;
320
- if (totalBytes > MAX_BODY_BYTES) {
321
- res.writeHead(413, { 'Content-Type': 'application/json' });
322
- res.end(JSON.stringify({ error: 'Request body too large', max: `${MAX_BODY_BYTES / 1024 / 1024}MB` }));
323
- return;
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
- chunks.push(buf);
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 && urlPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
330
- const cliResult = await handleViaCli(body, modelOverride, verbose);
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, { 'Content-Type': 'application/json' });
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
5
  "type": "module",
6
6
  "bin": {