@askalf/dario 3.4.0 → 3.4.3

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/proxy.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
3
- import { execSync, spawn } from 'node:child_process';
4
- import { readFileSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs';
3
+ import { execSync } from 'node:child_process';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { homedir, tmpdir } from 'node:os';
6
+ import { homedir } from 'node:os';
7
7
  import { arch, platform } from 'node:process';
8
8
  import { getAccessToken, getStatus } from './oauth.js';
9
9
  import { buildCCRequest, reverseMapResponse } from './cc-template.js';
@@ -13,7 +13,16 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts
13
13
  const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
14
14
  const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
15
15
  const MAX_CONCURRENT = 10; // Max concurrent upstream requests
16
- const LOCALHOST = '127.0.0.1';
16
+ const DEFAULT_HOST = '127.0.0.1';
17
+ // A host is "loopback" if it's one of the well-known localhost literals.
18
+ // Used to decide whether to warn at startup about binding to a reachable
19
+ // interface — binding anywhere else means other machines can reach the
20
+ // proxy and should only be done with DARIO_API_KEY set.
21
+ function isLoopbackHost(host) {
22
+ if (host === '127.0.0.1' || host === '::1' || host === 'localhost')
23
+ return true;
24
+ return host.startsWith('127.');
25
+ }
17
26
  // Simple semaphore for concurrency control
18
27
  class Semaphore {
19
28
  max;
@@ -36,63 +45,29 @@ class Semaphore {
36
45
  next();
37
46
  }
38
47
  }
39
- // Billing tag hash seed — extracted from Claude Code binary (constant XGA)
48
+ // Billing tag hash seed — matches Claude Code's value
40
49
  const BILLING_SEED = '59cf53e54c78';
41
- // Compute per-request build tag matching Claude Code's Oz$ algorithm:
50
+ // Compute per-request build tag:
42
51
  // SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)
43
52
  function computeBuildTag(userMessage, version) {
44
53
  const chars = [4, 7, 20].map(i => userMessage[i] || '0').join('');
45
54
  return createHash('sha256').update(`${BILLING_SEED}${chars}${version}`).digest('hex').slice(0, 3);
46
55
  }
47
- // Per-request cch: real Claude Code generates a random 5-char hex value each request.
48
- // Confirmed via MITM: 10 identical requests → 10 unique cch values, no deterministic pattern.
56
+ // Per-request cch: random 5-char hex value each request (Claude Code does the same).
49
57
  function computeCch() {
50
58
  return randomBytes(3).toString('hex').slice(0, 5);
51
59
  }
52
- // Detect installed Claude Code binary at startup (single exec for both version + availability)
53
- let cliAvailable = false;
54
- function detectCli() {
60
+ // Detect installed Claude Code version for the build-tag computation.
61
+ // Falls back to a known-good version if claude isn't on PATH.
62
+ function detectCliVersion() {
55
63
  try {
56
64
  const out = execSync('claude --version', { timeout: 5000, stdio: 'pipe' }).toString().trim();
57
- cliAvailable = true;
58
- // Capture major version (e.g., 2.1.100) — build tag is computed per-request
59
65
  return out.match(/^([\d]+\.[\d]+\.[\d]+)/)?.[1] ?? '2.1.100';
60
66
  }
61
67
  catch {
62
- cliAvailable = false;
63
68
  return '2.1.100';
64
69
  }
65
70
  }
66
- /** Convert a non-streaming Messages API response to SSE event stream. */
67
- function jsonToSse(jsonBody) {
68
- try {
69
- const msg = JSON.parse(jsonBody);
70
- const events = [];
71
- // message_start
72
- events.push(`event: message_start\ndata: ${JSON.stringify({ type: 'message_start', message: { ...msg, content: [], stop_reason: null } })}\n\n`);
73
- // content blocks
74
- const content = msg.content;
75
- if (content) {
76
- for (let i = 0; i < content.length; i++) {
77
- const block = content[i];
78
- events.push(`event: content_block_start\ndata: ${JSON.stringify({ type: 'content_block_start', index: i, content_block: { type: block.type, ...(block.type === 'text' ? { text: '' } : { thinking: '' }) } })}\n\n`);
79
- if (block.type === 'text' && block.text) {
80
- events.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: i, delta: { type: 'text_delta', text: block.text } })}\n\n`);
81
- }
82
- else if (block.type === 'thinking' && block.thinking) {
83
- events.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: i, delta: { type: 'thinking_delta', thinking: block.thinking } })}\n\n`);
84
- }
85
- events.push(`event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: i })}\n\n`);
86
- }
87
- }
88
- // message_stop
89
- events.push(`event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`);
90
- return events.join('');
91
- }
92
- catch {
93
- return '';
94
- }
95
- }
96
71
  /** Extract first user message text from a request body for billing tag computation. */
97
72
  function extractFirstUserMessage(body) {
98
73
  const messages = body.messages;
@@ -109,42 +84,8 @@ function extractFirstUserMessage(body) {
109
84
  }
110
85
  return '';
111
86
  }
112
- /** Convert CLI JSON response to OpenAI SSE format. */
113
- function jsonToOpenaiSse(jsonBody) {
114
- try {
115
- const parsed = JSON.parse(jsonBody);
116
- const text = parsed.content?.find(c => c.type === 'text')?.text ?? '';
117
- const ts = Math.floor(Date.now() / 1000);
118
- return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })}\n\n` +
119
- `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`;
120
- }
121
- catch {
122
- return '';
123
- }
124
- }
125
- /** Send a CLI result to the client, handling streaming/format translation. */
126
- function sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, securityHeaders) {
127
- const headers = { 'Access-Control-Allow-Origin': corsOrigin, ...securityHeaders };
128
- const ok = cliResult.status >= 200 && cliResult.status < 300;
129
- if (ok && clientWantsStream) {
130
- const sseData = isOpenAI ? jsonToOpenaiSse(cliResult.body) : jsonToSse(cliResult.body);
131
- if (sseData) {
132
- res.writeHead(200, { 'Content-Type': 'text/event-stream', ...headers });
133
- res.end(sseData);
134
- return;
135
- }
136
- }
137
- if (ok && isOpenAI) {
138
- try {
139
- cliResult.body = JSON.stringify(anthropicToOpenai(JSON.parse(cliResult.body)));
140
- }
141
- catch { }
142
- }
143
- res.writeHead(cliResult.status, { 'Content-Type': cliResult.contentType, ...headers });
144
- res.end(cliResult.body);
145
- }
146
- // Session ID rotates per request — each CC --print invocation creates a new session.
147
- // A persistent session ID across many requests is a detection signal.
87
+ // Session ID rotates per request fresh UUID per invocation.
88
+ // A persistent session ID across many requests is a behavioral fingerprint.
148
89
  let SESSION_ID = randomUUID();
149
90
  const OS_NAME = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'MacOS' : 'Linux';
150
91
  // Claude Code device identity — required for Max plan billing classification.
@@ -205,8 +146,7 @@ function filterBillableBetas(betas) {
205
146
  const ORCHESTRATION_TAG_NAMES = [
206
147
  'system-reminder', 'env', 'system_information', 'current_working_directory',
207
148
  'operating_system', 'default_shell', 'home_directory', 'task_metadata',
208
- 'tool_exec', 'tool_output', 'skill_content', 'skill_files',
209
- 'directories', 'available_skills', 'thinking',
149
+ 'directories', 'thinking',
210
150
  ];
211
151
  const ORCHESTRATION_PATTERNS = ORCHESTRATION_TAG_NAMES.flatMap(tag => [
212
152
  new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'),
@@ -375,125 +315,9 @@ function enrich429(body, headers) {
375
315
  return body;
376
316
  }
377
317
  }
378
- /**
379
- * CLI Backend: route requests through `claude --print` instead of direct API.
380
- * This bypasses rate limiting because Claude Code's binary has priority routing.
381
- */
382
- async function handleViaCli(body, model, verbose) {
383
- try {
384
- const parsed = JSON.parse(body.toString());
385
- // Extract the last user message as the prompt
386
- const messages = parsed.messages ?? [];
387
- const lastUser = [...messages].reverse().find(m => m.role === 'user');
388
- if (!lastUser) {
389
- return { status: 400, body: JSON.stringify({ error: 'No user message' }), contentType: 'application/json' };
390
- }
391
- const rawModel = model ?? parsed.model ?? 'claude-opus-4-6';
392
- // Validate model name — only allow alphanumeric, hyphens, dots, underscores
393
- const effectiveModel = /^[a-zA-Z0-9._-]+$/.test(rawModel) ? rawModel : 'claude-opus-4-6';
394
- const prompt = typeof lastUser.content === 'string'
395
- ? lastUser.content
396
- : JSON.stringify(lastUser.content);
397
- // Build claude --print command
398
- const args = ['--print', '--model', effectiveModel];
399
- // Flatten system prompt — API accepts string or array of content blocks,
400
- // but claude --print only accepts a string
401
- let systemPrompt = '';
402
- if (typeof parsed.system === 'string') {
403
- systemPrompt = parsed.system;
404
- }
405
- else if (Array.isArray(parsed.system)) {
406
- systemPrompt = parsed.system
407
- .filter(b => b.text)
408
- .map(b => b.text)
409
- .join('\n\n');
410
- }
411
- // Include conversation history as context
412
- const history = messages.slice(0, -1);
413
- if (history.length > 0) {
414
- const historyText = history.map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n');
415
- systemPrompt = systemPrompt ? `${systemPrompt}\n\nConversation history:\n${historyText}` : `Conversation history:\n${historyText}`;
416
- }
417
- // Write system prompt to temp file instead of passing as arg to avoid E2BIG
418
- // on large conversation contexts (OS arg size limit ~2MB)
419
- let systemPromptFile = null;
420
- if (systemPrompt) {
421
- systemPromptFile = join(tmpdir(), `dario-sysprompt-${randomUUID()}.txt`);
422
- writeFileSync(systemPromptFile, systemPrompt, { mode: 0o600 });
423
- args.push('--append-system-prompt-file', systemPromptFile);
424
- }
425
- if (verbose) {
426
- console.log(`[dario:cli] model=${effectiveModel} prompt=${prompt.substring(0, 60)}...`);
427
- }
428
- // Spawn claude --print
429
- return new Promise((resolve) => {
430
- // Cleanup temp file when done
431
- const cleanup = () => { if (systemPromptFile)
432
- try {
433
- unlinkSync(systemPromptFile);
434
- }
435
- catch { } };
436
- const child = spawn('claude', args, {
437
- stdio: ['pipe', 'pipe', 'pipe'],
438
- timeout: 300_000,
439
- });
440
- let stdout = '';
441
- let stderr = '';
442
- const MAX_CLI_OUTPUT = 5_000_000; // 5MB cap per stream — prevents OOM from runaway CLI
443
- child.stdout.on('data', (d) => { if (stdout.length < MAX_CLI_OUTPUT)
444
- stdout += d.toString(); });
445
- child.stderr.on('data', (d) => { if (stderr.length < MAX_CLI_OUTPUT)
446
- stderr += d.toString(); });
447
- child.stdin.write(prompt);
448
- child.stdin.end();
449
- child.on('close', (code) => {
450
- cleanup();
451
- if (code !== 0 || !stdout.trim()) {
452
- resolve({
453
- status: 502,
454
- body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: sanitizeError(stderr.substring(0, 200)) || 'CLI backend failed' } }),
455
- contentType: 'application/json',
456
- });
457
- return;
458
- }
459
- // Build a proper Messages API response
460
- const text = stdout.trim();
461
- const estimatedTokens = Math.ceil(text.length / 4);
462
- const response = {
463
- id: `msg_${randomUUID().replace(/-/g, '').substring(0, 24)}`,
464
- type: 'message',
465
- role: 'assistant',
466
- model: effectiveModel,
467
- content: [{ type: 'text', text }],
468
- stop_reason: 'end_turn',
469
- stop_sequence: null,
470
- usage: {
471
- input_tokens: Math.ceil(prompt.length / 4),
472
- output_tokens: estimatedTokens,
473
- },
474
- };
475
- resolve({ status: 200, body: JSON.stringify(response), contentType: 'application/json' });
476
- });
477
- child.on('error', (err) => {
478
- cleanup();
479
- resolve({
480
- status: 502,
481
- body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'Claude CLI not found. Install Claude Code first.' } }),
482
- contentType: 'application/json',
483
- });
484
- });
485
- });
486
- }
487
- catch (err) {
488
- return {
489
- status: 400,
490
- body: JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid request body' } }),
491
- contentType: 'application/json',
492
- };
493
- }
494
- }
495
318
  export async function startProxy(opts = {}) {
496
319
  const port = opts.port ?? DEFAULT_PORT;
320
+ const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
497
321
  const verbose = opts.verbose ?? false;
498
322
  const passthrough = opts.passthrough ?? false;
499
323
  // Verify auth before starting
@@ -502,7 +326,7 @@ export async function startProxy(opts = {}) {
502
326
  console.error('[dario] Not authenticated. Run `dario login` first.');
503
327
  process.exit(1);
504
328
  }
505
- const cliVersion = detectCli();
329
+ const cliVersion = detectCliVersion();
506
330
  const modelOverride = opts.model ? (MODEL_ALIASES[opts.model] ?? opts.model) : null;
507
331
  const identity = loadClaudeIdentity();
508
332
  if (identity.deviceId) {
@@ -512,7 +336,7 @@ export async function startProxy(opts = {}) {
512
336
  console.warn('[dario] WARNING: No Claude Code device identity found. Requests may be billed as Extra Usage.');
513
337
  console.warn('[dario] Run Claude Code at least once to generate ~/.claude/.claude.json');
514
338
  }
515
- // Pre-build static headers (matches real Claude Code captured via MITM)
339
+ // Pre-build static headers matches the set a real Claude Code client sends.
516
340
  const staticHeaders = passthrough ? {
517
341
  'accept': 'application/json',
518
342
  'Content-Type': 'application/json',
@@ -531,18 +355,20 @@ export async function startProxy(opts = {}) {
531
355
  // Claude Code runs on Bun which reports v24.3.0 as Node compat version
532
356
  'x-stainless-runtime-version': 'v24.3.0',
533
357
  };
534
- const useCli = opts.cliBackend ?? false;
535
358
  let requestCount = 0;
536
359
  const semaphore = new Semaphore(MAX_CONCURRENT);
537
- // Rate governor: CC --print takes ~2-3s per invocation.
538
- // Rapid-fire requests from one "session" is an inhuman pattern.
539
- // Minimum 500ms between requests — fast enough for agents, slow enough to not flag.
360
+ // Rate governor minimum 500ms between requests. Fast enough for agents,
361
+ // slow enough to not look like a scripted flood of identical traffic.
540
362
  let lastRequestTime = 0;
541
363
  const MIN_REQUEST_INTERVAL_MS = parseInt(process.env.DARIO_MIN_INTERVAL_MS || '500', 10);
542
364
  // Optional proxy authentication — pre-encode key buffer for performance
543
365
  const apiKey = process.env.DARIO_API_KEY;
544
366
  const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
545
- const corsOrigin = `http://localhost:${port}`;
367
+ // CORS origin defaults to the localhost URL the proxy is served at. Users
368
+ // binding to a non-loopback address (e.g. a Tailscale interface) can
369
+ // override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
370
+ // dario over the mesh will be blocked by their browser's CORS check.
371
+ const corsOrigin = process.env.DARIO_CORS_ORIGIN || `http://localhost:${port}`;
546
372
  // Security headers for all responses
547
373
  const SECURITY_HEADERS = {
548
374
  'X-Content-Type-Options': 'nosniff',
@@ -656,31 +482,6 @@ export async function startProxy(opts = {}) {
656
482
  clearTimeout(bodyTimeout);
657
483
  }
658
484
  const body = Buffer.concat(chunks);
659
- // CLI backend mode: route through claude --print (works for both Anthropic and OpenAI endpoints)
660
- if (useCli && req.method === 'POST' && body.length > 0) {
661
- let cliBody = body;
662
- let clientWantsStream = false;
663
- // Translate OpenAI format before passing to CLI
664
- if (isOpenAI) {
665
- try {
666
- const parsed = JSON.parse(body.toString());
667
- clientWantsStream = !!parsed.stream;
668
- cliBody = Buffer.from(JSON.stringify(openaiToAnthropic(parsed, modelOverride)));
669
- }
670
- catch { /* send as-is */ }
671
- }
672
- else {
673
- try {
674
- const parsed = JSON.parse(body.toString());
675
- clientWantsStream = !!parsed.stream;
676
- }
677
- catch { }
678
- }
679
- const cliResult = await handleViaCli(cliBody, modelOverride, verbose);
680
- requestCount++;
681
- sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, SECURITY_HEADERS);
682
- return;
683
- }
684
485
  // Parse body once, apply OpenAI translation, model override, and sanitization
685
486
  let finalBody = body.length > 0 ? body : undefined;
686
487
  let ccToolMap = null;
@@ -731,7 +532,7 @@ export async function startProxy(opts = {}) {
731
532
  beta += ',' + clientBeta;
732
533
  }
733
534
  else {
734
- // CC v2.1.104 beta set (exact 8 from MITM capture, exact order).
535
+ // CC v2.1.104 beta set 8 flags in the order Claude Code sends them.
735
536
  // context-1m requires Extra Usage — if it 400s, we auto-retry without it.
736
537
  beta = 'claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24';
737
538
  if (clientBeta) {
@@ -749,7 +550,7 @@ export async function startProxy(opts = {}) {
749
550
  await new Promise(r => setTimeout(r, MIN_REQUEST_INTERVAL_MS - elapsed));
750
551
  }
751
552
  lastRequestTime = Date.now();
752
- // Rotate session ID per request — CC --print creates a new session each invocation
553
+ // Rotate session ID per request — fresh UUID avoids persistent-session fingerprinting
753
554
  SESSION_ID = randomUUID();
754
555
  const headers = {
755
556
  ...staticHeaders,
@@ -798,24 +599,20 @@ export async function startProxy(opts = {}) {
798
599
  else if (upstream.status === 429) {
799
600
  // Not a context-1m issue — return enriched 429 directly
800
601
  const enriched = enrich429(peekedBody, upstream.headers);
801
- if (!(cliAvailable && !useCli)) {
802
- const responseHeaders = {
803
- 'Content-Type': 'application/json',
804
- 'Access-Control-Allow-Origin': corsOrigin,
805
- ...SECURITY_HEADERS,
806
- };
807
- for (const [key, value] of upstream.headers.entries()) {
808
- if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
809
- responseHeaders[key] = value;
810
- }
602
+ const responseHeaders = {
603
+ 'Content-Type': 'application/json',
604
+ 'Access-Control-Allow-Origin': corsOrigin,
605
+ ...SECURITY_HEADERS,
606
+ };
607
+ for (const [key, value] of upstream.headers.entries()) {
608
+ if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
609
+ responseHeaders[key] = value;
811
610
  }
812
- requestCount++;
813
- res.writeHead(429, responseHeaders);
814
- res.end(enriched);
815
- return;
816
611
  }
817
- // Fall through to CLI fallback below — need to re-handle 429 with
818
- // already-consumed body; stash it for the fallback path.
612
+ requestCount++;
613
+ res.writeHead(429, responseHeaders);
614
+ res.end(enriched);
615
+ return;
819
616
  }
820
617
  else if (upstream.status === 400) {
821
618
  // Non-long-context 400 — forward upstream error directly.
@@ -836,7 +633,7 @@ export async function startProxy(opts = {}) {
836
633
  }
837
634
  }
838
635
  // Enrich 429 errors with rate limit details from headers (Anthropic only returns "Error")
839
- if (upstream.status === 429 && !(cliAvailable && !useCli)) {
636
+ if (upstream.status === 429) {
840
637
  const errBody = await upstream.text().catch(() => '');
841
638
  const enriched = enrich429(errBody, upstream.headers);
842
639
  const responseHeaders = {
@@ -854,42 +651,6 @@ export async function startProxy(opts = {}) {
854
651
  res.end(enriched);
855
652
  return;
856
653
  }
857
- // Auto-fallback: if API returns 429 and CLI is available, retry through CLI binary
858
- if (upstream.status === 429 && cliAvailable && !useCli) {
859
- const errBody429 = await upstream.text().catch(() => '');
860
- if (verbose)
861
- console.log(`[dario] #${requestCount} 429 from API — falling back to CLI`);
862
- let clientWantsStream = false;
863
- try {
864
- clientWantsStream = !!JSON.parse(body.toString()).stream;
865
- }
866
- catch { }
867
- const cliResult = await handleViaCli(body, modelOverride, verbose);
868
- // If CLI fallback also failed, return the original 429 with enriched details
869
- // instead of a cryptic 502 from CLI failure
870
- if (cliResult.status >= 500) {
871
- if (verbose)
872
- console.log(`[dario] #${requestCount} CLI fallback failed (${cliResult.status}) — returning original 429`);
873
- const enriched = enrich429(errBody429, upstream.headers);
874
- const responseHeaders = {
875
- 'Content-Type': 'application/json',
876
- 'Access-Control-Allow-Origin': corsOrigin,
877
- ...SECURITY_HEADERS,
878
- };
879
- for (const [key, value] of upstream.headers.entries()) {
880
- if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
881
- responseHeaders[key] = value;
882
- }
883
- }
884
- requestCount++;
885
- res.writeHead(429, responseHeaders);
886
- res.end(enriched);
887
- return;
888
- }
889
- requestCount++;
890
- sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, SECURITY_HEADERS);
891
- return;
892
- }
893
654
  // Detect streaming from content-type (reliable) or body (fallback)
894
655
  const contentType = upstream.headers.get('content-type') ?? '';
895
656
  const isStream = contentType.includes('text/event-stream');
@@ -1007,24 +768,43 @@ export async function startProxy(opts = {}) {
1007
768
  }
1008
769
  process.exit(1);
1009
770
  });
1010
- server.listen(port, LOCALHOST, () => {
1011
- const modeLine = passthrough ? 'Mode: passthrough (OAuth swap only, no injection)' : useCli ? 'Backend: Claude CLI (bypasses rate limits)' : `OAuth: ${status.status} (expires in ${status.expiresIn})`;
771
+ server.listen(port, host, () => {
772
+ const modeLine = passthrough
773
+ ? 'Mode: passthrough (OAuth swap only, no injection)'
774
+ : `OAuth: ${status.status} (expires in ${status.expiresIn})`;
1012
775
  const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
776
+ // Display URL uses `localhost` for loopback binds and the literal host
777
+ // for exposed binds, so the printed URL is the one a client would
778
+ // actually use to reach the proxy.
779
+ const displayHost = isLoopbackHost(host) ? 'localhost' : host;
1013
780
  console.log('');
1014
- console.log(` dario — http://localhost:${port}`);
781
+ console.log(` dario — http://${displayHost}:${port}`);
1015
782
  console.log('');
1016
783
  console.log(' Your Claude subscription is now an API.');
1017
784
  console.log('');
1018
785
  console.log(' Usage:');
1019
- console.log(` ANTHROPIC_BASE_URL=http://localhost:${port}`);
786
+ console.log(` ANTHROPIC_BASE_URL=http://${displayHost}:${port}`);
1020
787
  console.log(' ANTHROPIC_API_KEY=dario');
1021
788
  console.log('');
1022
789
  console.log(` ${modeLine}`);
1023
790
  console.log(` ${modelLine}`);
791
+ if (!isLoopbackHost(host)) {
792
+ console.log('');
793
+ console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
794
+ if (!apiKey) {
795
+ console.log(' DARIO_API_KEY is not set. Any host that can reach this port can');
796
+ console.log(' proxy requests through your OAuth subscription. Set DARIO_API_KEY');
797
+ console.log(' before exposing dario beyond loopback.');
798
+ }
799
+ else {
800
+ console.log(' DARIO_API_KEY is set — clients must send x-api-key or Authorization');
801
+ console.log(' to be accepted.');
802
+ }
803
+ }
1024
804
  console.log('');
1025
805
  });
1026
- // Session presence heartbeat — registers this proxy as an active Claude Code session
1027
- // Claude Code sends this every 5 seconds; the server uses it for priority routing
806
+ // Session presence heartbeat — keeps the OAuth session marked active
807
+ // (matches the ~5s cadence of a real Claude Code session).
1028
808
  const clientId = randomUUID();
1029
809
  const connectedAt = new Date().toISOString();
1030
810
  let lastPresencePulse = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.4.0",
3
+ "version": "3.4.3",
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": {
@@ -26,7 +26,9 @@
26
26
  "start": "node dist/cli.js",
27
27
  "dev": "tsx src/cli.ts",
28
28
  "e2e": "node test/e2e.mjs",
29
- "compat": "node test/compat.mjs"
29
+ "compat": "node test/compat.mjs",
30
+ "lint:pkg": "node scripts/check-package-json.mjs",
31
+ "fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
30
32
  },
31
33
  "keywords": [
32
34
  "claude",