@askalf/dario 2.0.0 → 2.1.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
@@ -27,12 +27,12 @@
27
27
  ```bash
28
28
  npx @askalf/dario login # detects Claude Code credentials, starts proxy
29
29
 
30
- # now use it from anywhere
31
- export ANTHROPIC_BASE_URL=http://localhost:3456
32
- export ANTHROPIC_API_KEY=dario
30
+ # now use it from anywhere — Anthropic or OpenAI SDK
31
+ export ANTHROPIC_BASE_URL=http://localhost:3456 # or OPENAI_BASE_URL=http://localhost:3456/v1
32
+ export ANTHROPIC_API_KEY=dario # or OPENAI_API_KEY=dario
33
33
  ```
34
34
 
35
- That's it. Any tool that speaks the Anthropic API now uses your subscription.
35
+ Opus, Sonnet, Haiku — all models, streaming, tool use. Works with OpenClaw, Cursor, Continue, Aider, Hermes, or any tool that speaks the Anthropic or OpenAI API. When rate limited, `--cli` routes through Claude Code for uninterrupted Opus access.
36
36
 
37
37
  ---
38
38
 
@@ -88,6 +88,7 @@ Usage:
88
88
  ANTHROPIC_BASE_URL=http://localhost:3456
89
89
  ANTHROPIC_API_KEY=dario
90
90
 
91
+ Auth: open (no DARIO_API_KEY set)
91
92
  OAuth: healthy (expires in 11h 42m)
92
93
  Model: passthrough (client decides)
93
94
  ```
@@ -159,34 +160,18 @@ dario proxy --cli --model=opus
159
160
 
160
161
  ## OpenAI Compatibility
161
162
 
162
- Dario speaks both Anthropic and OpenAI API formats. Any tool built for OpenAI works with your Claude subscription Cursor, Continue, LiteLLM, anything.
163
+ Dario implements `/v1/chat/completions` any tool built for the OpenAI API works with your Claude subscription. No code changes needed.
163
164
 
164
165
  ```bash
165
- # Use with any OpenAI SDK or tool
166
+ dario proxy --model=opus
167
+
166
168
  export OPENAI_BASE_URL=http://localhost:3456/v1
167
169
  export OPENAI_API_KEY=dario
168
- ```
169
-
170
- ```python
171
- from openai import OpenAI
172
170
 
173
- client = OpenAI(base_url="http://localhost:3456/v1", api_key="dario")
174
- response = client.chat.completions.create(
175
- model="claude-opus-4-6", # or use "gpt-4" — auto-maps to Opus
176
- messages=[{"role": "user", "content": "Hello!"}]
177
- )
171
+ # Cursor, Continue, LiteLLM, any OpenAI SDK — all work
178
172
  ```
179
173
 
180
- Model mapping (automatic):
181
-
182
- | OpenAI model | Maps to |
183
- |---|---|
184
- | `gpt-4`, `gpt-4o`, `o1`, `o3` | `claude-opus-4-6` |
185
- | `o1-mini`, `o3-mini` | `claude-sonnet-4-6` |
186
- | `gpt-3.5-turbo`, `gpt-4o-mini` | `claude-haiku-4-5` |
187
- | Any `claude-*` model | Passed through directly |
188
-
189
- Streaming, system prompts, temperature, and stop sequences all translate automatically.
174
+ Use `--model=opus` to force the model regardless of what the client sends. Or pass `claude-opus-4-6` as the model name directly — Claude model names work as-is.
190
175
 
191
176
  ## Usage Examples
192
177
 
@@ -310,12 +295,13 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
310
295
 
311
296
  ### Proxy Options
312
297
 
313
- | Flag | Description | Default |
314
- |------|-------------|---------|
298
+ | Flag/Env | Description | Default |
299
+ |----------|-------------|---------|
315
300
  | `--cli` | Use Claude CLI as backend (bypasses rate limits) | off |
316
301
  | `--model=MODEL` | Force a model (`opus`, `sonnet`, `haiku`, or full ID) | passthrough |
317
302
  | `--port=PORT` | Port to listen on | `3456` |
318
303
  | `--verbose` / `-v` | Log every request | off |
304
+ | `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` header or `Authorization: Bearer` header | unset (open) |
319
305
 
320
306
  ## Supported Features
321
307
 
package/dist/cli.js CHANGED
@@ -13,9 +13,9 @@ import { readFile, unlink } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
14
  import { homedir } from 'node:os';
15
15
  import { startAutoOAuthFlow, getStatus, refreshTokens } from './oauth.js';
16
- import { startProxy } from './proxy.js';
16
+ import { startProxy, sanitizeError } from './proxy.js';
17
17
  const args = process.argv.slice(2);
18
- const command = args[0] ?? (process.stdin.isTTY ? 'proxy' : 'proxy');
18
+ const command = args[0] ?? 'proxy';
19
19
  async function login() {
20
20
  console.log('');
21
21
  console.log(' dario — Claude Login');
@@ -50,7 +50,7 @@ async function login() {
50
50
  }
51
51
  catch (err) {
52
52
  console.error('');
53
- console.error(` Login failed: ${err instanceof Error ? err.message : err}`);
53
+ console.error(` Login failed: ${sanitizeError(err)}`);
54
54
  console.error(' Try again with `dario login`.');
55
55
  process.exit(1);
56
56
  }
@@ -89,7 +89,7 @@ async function refresh() {
89
89
  console.log(`[dario] Token refreshed. Expires in ${expiresIn} minutes.`);
90
90
  }
91
91
  catch (err) {
92
- console.error(`[dario] Refresh failed: ${err instanceof Error ? err.message : err}`);
92
+ console.error(`[dario] Refresh failed: ${sanitizeError(err)}`);
93
93
  process.exit(1);
94
94
  }
95
95
  }
@@ -172,7 +172,6 @@ if (!handler) {
172
172
  process.exit(1);
173
173
  }
174
174
  handler().catch(err => {
175
- const msg = err instanceof Error ? err.message : String(err);
176
- console.error('Fatal error:', msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]'));
175
+ console.error('Fatal error:', sanitizeError(err));
177
176
  process.exit(1);
178
177
  });
package/dist/index.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  */
7
7
  export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
8
8
  export type { OAuthTokens, CredentialsFile } from './oauth.js';
9
- export { startProxy } from './proxy.js';
9
+ export { startProxy, sanitizeError } from './proxy.js';
package/dist/index.js CHANGED
@@ -5,4 +5,4 @@
5
5
  * instead of running the CLI.
6
6
  */
7
7
  export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
8
- export { startProxy } from './proxy.js';
8
+ export { startProxy, sanitizeError } from './proxy.js';
package/dist/proxy.d.ts CHANGED
@@ -13,5 +13,6 @@ interface ProxyOptions {
13
13
  model?: string;
14
14
  cliBackend?: boolean;
15
15
  }
16
+ export declare function sanitizeError(err: unknown): string;
16
17
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
17
18
  export {};
package/dist/proxy.js CHANGED
@@ -8,7 +8,7 @@
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';
11
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
12
12
  import { execSync, spawn } from 'node:child_process';
13
13
  import { arch, platform, version as nodeVersion } from 'node:process';
14
14
  import { getAccessToken, getStatus } from './oauth.js';
@@ -17,7 +17,6 @@ const DEFAULT_PORT = 3456;
17
17
  const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
18
18
  const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
19
19
  const LOCALHOST = '127.0.0.1';
20
- const CORS_ORIGIN = 'http://localhost';
21
20
  // Detect installed Claude Code version at startup
22
21
  function detectClaudeVersion() {
23
22
  try {
@@ -57,16 +56,20 @@ const MODEL_ALIASES = {
57
56
  };
58
57
  // OpenAI model name → Anthropic model name
59
58
  const OPENAI_MODEL_MAP = {
60
- 'gpt-4': 'claude-opus-4-6',
59
+ 'gpt-4.1': 'claude-opus-4-6',
60
+ 'gpt-4.1-mini': 'claude-sonnet-4-6',
61
+ 'gpt-4.1-nano': 'claude-haiku-4-5',
61
62
  'gpt-4o': 'claude-opus-4-6',
62
- 'gpt-4-turbo': 'claude-opus-4-6',
63
63
  'gpt-4o-mini': 'claude-haiku-4-5',
64
+ 'gpt-4-turbo': 'claude-opus-4-6',
65
+ 'gpt-4': 'claude-opus-4-6',
64
66
  'gpt-3.5-turbo': 'claude-haiku-4-5',
65
- 'o1': 'claude-opus-4-6',
66
- 'o1-mini': 'claude-sonnet-4-6',
67
- 'o1-preview': 'claude-opus-4-6',
68
67
  'o3': 'claude-opus-4-6',
69
68
  'o3-mini': 'claude-sonnet-4-6',
69
+ 'o4-mini': 'claude-sonnet-4-6',
70
+ 'o1': 'claude-opus-4-6',
71
+ 'o1-mini': 'claude-sonnet-4-6',
72
+ 'o1-pro': 'claude-opus-4-6',
70
73
  };
71
74
  /**
72
75
  * Translate OpenAI chat completion request → Anthropic Messages request.
@@ -177,10 +180,13 @@ function openaiModelsList() {
177
180
  })),
178
181
  };
179
182
  }
180
- function sanitizeError(err) {
183
+ export function sanitizeError(err) {
181
184
  const msg = err instanceof Error ? err.message : String(err);
182
- // Never leak tokens in error messages
183
- return msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]');
185
+ // Never leak tokens, JWTs, or bearer values in error messages
186
+ return msg
187
+ .replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]')
188
+ .replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
189
+ .replace(/Bearer\s+[a-zA-Z0-9_-]+/gi, 'Bearer [REDACTED]');
184
190
  }
185
191
  /**
186
192
  * CLI Backend: route requests through `claude --print` instead of direct API.
@@ -195,7 +201,9 @@ async function handleViaCli(body, model, verbose) {
195
201
  if (!lastUser) {
196
202
  return { status: 400, body: JSON.stringify({ error: 'No user message' }), contentType: 'application/json' };
197
203
  }
198
- const effectiveModel = model ?? parsed.model ?? 'claude-opus-4-6';
204
+ const rawModel = model ?? parsed.model ?? 'claude-opus-4-6';
205
+ // Validate model name — only allow alphanumeric, hyphens, dots, underscores
206
+ const effectiveModel = /^[a-zA-Z0-9._-]+$/.test(rawModel) ? rawModel : 'claude-opus-4-6';
199
207
  const prompt = typeof lastUser.content === 'string'
200
208
  ? lastUser.content
201
209
  : JSON.stringify(lastUser.content);
@@ -231,7 +239,7 @@ async function handleViaCli(body, model, verbose) {
231
239
  if (code !== 0 || !stdout.trim()) {
232
240
  resolve({
233
241
  status: 502,
234
- body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: stderr.substring(0, 200) || 'CLI backend failed' } }),
242
+ body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: sanitizeError(stderr.substring(0, 200)) || 'CLI backend failed' } }),
235
243
  contentType: 'application/json',
236
244
  });
237
245
  return;
@@ -286,11 +294,28 @@ export async function startProxy(opts = {}) {
286
294
  const useCli = opts.cliBackend ?? false;
287
295
  let requestCount = 0;
288
296
  let tokenCostEstimate = 0;
297
+ // Optional proxy authentication
298
+ const apiKey = process.env.DARIO_API_KEY;
299
+ const corsOrigin = `http://localhost:${port}`;
300
+ function checkAuth(req) {
301
+ if (!apiKey)
302
+ return true; // no key set = open access
303
+ const provided = req.headers['x-api-key']
304
+ || req.headers.authorization?.replace(/^Bearer\s+/i, '');
305
+ if (!provided)
306
+ return false;
307
+ try {
308
+ return timingSafeEqual(Buffer.from(provided), Buffer.from(apiKey));
309
+ }
310
+ catch {
311
+ return false;
312
+ }
313
+ }
289
314
  const server = createServer(async (req, res) => {
290
315
  // CORS preflight
291
316
  if (req.method === 'OPTIONS') {
292
317
  res.writeHead(204, {
293
- 'Access-Control-Allow-Origin': CORS_ORIGIN,
318
+ 'Access-Control-Allow-Origin': corsOrigin,
294
319
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
295
320
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
296
321
  'Access-Control-Max-Age': '86400',
@@ -312,6 +337,12 @@ export async function startProxy(opts = {}) {
312
337
  }));
313
338
  return;
314
339
  }
340
+ // Auth gate — everything below health requires auth when DARIO_API_KEY is set
341
+ if (!checkAuth(req)) {
342
+ res.writeHead(401, { 'Content-Type': 'application/json' });
343
+ res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' }));
344
+ return;
345
+ }
315
346
  // Status endpoint
316
347
  if (urlPath === '/status') {
317
348
  const s = await getStatus();
@@ -322,7 +353,7 @@ export async function startProxy(opts = {}) {
322
353
  // OpenAI-compatible models list
323
354
  if (urlPath === '/v1/models' && req.method === 'GET') {
324
355
  requestCount++;
325
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': CORS_ORIGIN });
356
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': corsOrigin });
326
357
  res.end(JSON.stringify(openaiModelsList()));
327
358
  return;
328
359
  }
@@ -368,7 +399,7 @@ export async function startProxy(opts = {}) {
368
399
  requestCount++;
369
400
  res.writeHead(cliResult.status, {
370
401
  'Content-Type': cliResult.contentType,
371
- 'Access-Control-Allow-Origin': CORS_ORIGIN,
402
+ 'Access-Control-Allow-Origin': corsOrigin,
372
403
  });
373
404
  res.end(cliResult.body);
374
405
  return;
@@ -447,7 +478,7 @@ export async function startProxy(opts = {}) {
447
478
  // Forward response headers
448
479
  const responseHeaders = {
449
480
  'Content-Type': contentType || 'application/json',
450
- 'Access-Control-Allow-Origin': CORS_ORIGIN,
481
+ 'Access-Control-Allow-Origin': corsOrigin,
451
482
  };
452
483
  // Forward rate limit headers (including unified subscription headers)
453
484
  for (const [key, value] of upstream.headers.entries()) {
@@ -463,6 +494,7 @@ export async function startProxy(opts = {}) {
463
494
  const decoder = new TextDecoder();
464
495
  try {
465
496
  let buffer = '';
497
+ const MAX_LINE_LENGTH = 1_000_000; // 1MB max per SSE line
466
498
  while (true) {
467
499
  const { done, value } = await reader.read();
468
500
  if (done)
@@ -470,6 +502,10 @@ export async function startProxy(opts = {}) {
470
502
  if (isOpenAI) {
471
503
  // Translate Anthropic SSE → OpenAI SSE
472
504
  buffer += decoder.decode(value, { stream: true });
505
+ // Guard against unbounded buffer growth
506
+ if (buffer.length > MAX_LINE_LENGTH) {
507
+ buffer = buffer.slice(-MAX_LINE_LENGTH);
508
+ }
473
509
  const lines = buffer.split('\n');
474
510
  buffer = lines.pop() ?? '';
475
511
  for (const line of lines) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "2.0.0",
3
+ "version": "2.1.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": {