@askalf/dario 3.31.14 → 3.31.16

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/accounts.js CHANGED
@@ -23,6 +23,8 @@ import { randomUUID, randomBytes, createHash } from 'node:crypto';
23
23
  import { createServer } from 'node:http';
24
24
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
25
25
  import { loadCredentials } from './oauth.js';
26
+ import { openBrowser } from './open-browser.js';
27
+ import { redactSecrets } from './redact.js';
26
28
  const DARIO_DIR = join(homedir(), '.dario');
27
29
  const ACCOUNTS_DIR = join(DARIO_DIR, 'accounts');
28
30
  /**
@@ -161,7 +163,10 @@ async function doRefreshAccountToken(creds) {
161
163
  });
162
164
  if (!res.ok) {
163
165
  const errBody = await res.text().catch(() => '');
164
- throw new Error(`Refresh failed for ${creds.alias} (${res.status}): ${errBody.slice(0, 200)}`);
166
+ // Redact tokens / JWTs / Bearer values before they hit the Error
167
+ // message — defense-in-depth against an upstream that ever echoes a
168
+ // credential into a 4xx body. See src/redact.ts.
169
+ throw new Error(`Refresh failed for ${creds.alias} (${res.status}): ${redactSecrets(errBody.slice(0, 200))}`);
165
170
  }
166
171
  const data = await res.json();
167
172
  const updated = {
@@ -186,13 +191,10 @@ function generatePKCE() {
186
191
  const codeChallenge = base64url(createHash('sha256').update(codeVerifier).digest());
187
192
  return { codeVerifier, codeChallenge };
188
193
  }
189
- function openBrowser(url) {
190
- const { exec } = require('node:child_process');
191
- const cmd = process.platform === 'win32' ? `start "" "${url}"`
192
- : process.platform === 'darwin' ? `open "${url}"`
193
- : `xdg-open "${url}"`;
194
- exec(cmd, () => { });
195
- }
194
+ // `openBrowser` lives in src/open-browser.ts — uses execFile + argv array
195
+ // + URL-protocol allowlist instead of shell interpolation. The previous
196
+ // inline `exec(\`start "" "${url}"\`)` pattern would have shelled out
197
+ // any `&` / `|` / `^` / backtick / `$()` in a URL.
196
198
  /**
197
199
  * Interactive OAuth flow that adds a new account to the pool. Uses dario's
198
200
  * auto-detected CC OAuth config (same scanner the single-account path uses).
@@ -256,7 +258,7 @@ export async function addAccountViaOAuth(alias) {
256
258
  });
257
259
  if (!tokenRes.ok) {
258
260
  const body = await tokenRes.text().catch(() => '');
259
- throw new Error(`Token exchange failed (${tokenRes.status}): ${body.slice(0, 200)}`);
261
+ throw new Error(`Token exchange failed (${tokenRes.status}): ${redactSecrets(body.slice(0, 200))}`);
260
262
  }
261
263
  const tokens = await tokenRes.json();
262
264
  // Prefer CC identity if installed; otherwise generate fresh IDs.
@@ -299,7 +301,10 @@ export async function addAccountViaOAuth(alias) {
299
301
  console.log(` If the browser didn't open, visit:`);
300
302
  console.log(` ${authUrl}`);
301
303
  console.log();
302
- openBrowser(authUrl);
304
+ try {
305
+ openBrowser(authUrl);
306
+ }
307
+ catch { /* non-fatal: user has the URL printed above */ }
303
308
  });
304
309
  server.on('error', (err) => {
305
310
  reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
package/dist/cli.js CHANGED
@@ -12,34 +12,22 @@
12
12
  // ── Bun auto-relaunch ──
13
13
  // Bun's TLS fingerprint matches Claude Code's runtime (both use Bun/BoringSSL).
14
14
  // If Bun is installed and we're running on Node, relaunch under Bun for
15
- // network-level fingerprint fidelity.
16
- if (!('Bun' in globalThis) && !process.env.DARIO_NO_BUN) {
17
- try {
18
- const { execFileSync } = await import('node:child_process');
19
- // Check if bun exists
20
- execFileSync('bun', ['--version'], { stdio: 'ignore', timeout: 3000 });
21
- // Relaunch under bun
22
- const { spawn } = await import('node:child_process');
23
- const child = spawn('bun', ['run', ...process.argv.slice(1)], {
24
- stdio: 'inherit',
25
- env: { ...process.env, DARIO_NO_BUN: '1' },
26
- });
27
- child.on('exit', (code) => process.exit(code ?? 0));
28
- // Prevent this process from continuing
29
- await new Promise(() => { });
30
- }
31
- catch {
32
- // Bun not available, continue with Node
33
- }
34
- }
15
+ // network-level fingerprint fidelity. Moved below into the main-entry guard
16
+ // at the bottom of the file so importing this module (e.g. from tests that
17
+ // just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
18
+ // other startup side effect.
35
19
  import { unlink } from 'node:fs/promises';
36
20
  import { join } from 'node:path';
37
21
  import { homedir } from 'node:os';
22
+ import { pathToFileURL } from 'node:url';
38
23
  import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
39
24
  import { startProxy, sanitizeError } from './proxy.js';
40
25
  import { VALID_EFFORT_VALUES } from './cc-template.js';
41
26
  import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
42
27
  import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
28
+ // `args` / `command` at module scope — command handlers below close over
29
+ // `args` to read their own flags. Reading argv is harmless on import; only
30
+ // the handler dispatch at the bottom is gated behind the main-entry check.
43
31
  const args = process.argv.slice(2);
44
32
  const command = args[0] ?? 'proxy';
45
33
  async function login() {
@@ -1159,13 +1147,42 @@ const commands = {
1159
1147
  '--version': version,
1160
1148
  '-V': version,
1161
1149
  };
1162
- const handler = commands[command];
1163
- if (!handler) {
1164
- console.error(`Unknown command: ${command}`);
1165
- console.error('Run `dario help` for usage.');
1166
- process.exit(1);
1150
+ // Main-entry guard. Only run the Bun auto-relaunch and handler dispatch when
1151
+ // this module is the direct entry point — importing it (from tests or for
1152
+ // a library helper like `parsePositiveIntEnv`) must NOT start the proxy.
1153
+ // Before this guard, `import { parsePositiveIntEnv } from './cli.js'` would
1154
+ // fall through to `command = args[0] ?? 'proxy'` and fire `handler()`, which
1155
+ // tried to run `startProxy()` and failed the test with "Not authenticated".
1156
+ const isDirectEntry = typeof process.argv[1] === 'string' &&
1157
+ import.meta.url === pathToFileURL(process.argv[1]).href;
1158
+ if (isDirectEntry) {
1159
+ // Bun auto-relaunch for TLS fingerprint fidelity. Only meaningful when
1160
+ // dario is the direct entry — if we're imported, whoever imported us
1161
+ // already chose their runtime.
1162
+ if (!('Bun' in globalThis) && !process.env.DARIO_NO_BUN) {
1163
+ try {
1164
+ const { execFileSync, spawn } = await import('node:child_process');
1165
+ execFileSync('bun', ['--version'], { stdio: 'ignore', timeout: 3000 });
1166
+ const child = spawn('bun', ['run', ...process.argv.slice(1)], {
1167
+ stdio: 'inherit',
1168
+ env: { ...process.env, DARIO_NO_BUN: '1' },
1169
+ });
1170
+ child.on('exit', (code) => process.exit(code ?? 0));
1171
+ // Prevent this process from continuing
1172
+ await new Promise(() => { });
1173
+ }
1174
+ catch {
1175
+ // Bun not available, continue with Node
1176
+ }
1177
+ }
1178
+ const handler = commands[command];
1179
+ if (!handler) {
1180
+ console.error(`Unknown command: ${command}`);
1181
+ console.error('Run `dario help` for usage.');
1182
+ process.exit(1);
1183
+ }
1184
+ handler().catch(err => {
1185
+ console.error('Fatal error:', sanitizeError(err));
1186
+ process.exit(1);
1187
+ });
1167
1188
  }
1168
- handler().catch(err => {
1169
- console.error('Fatal error:', sanitizeError(err));
1170
- process.exit(1);
1171
- });
package/dist/oauth.js CHANGED
@@ -11,6 +11,7 @@ import { execFile } from 'node:child_process';
11
11
  import { dirname, join } from 'node:path';
12
12
  import { homedir, platform } from 'node:os';
13
13
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
14
+ import { redactSecrets } from './redact.js';
14
15
  // Manual-flow redirect URI. Anthropic's authorize endpoint special-cases
15
16
  // this value (also baked into CC as MANUAL_REDIRECT_URL) to render the
16
17
  // authorization code + state on a copy-paste success page instead of
@@ -255,12 +256,15 @@ export async function startAutoOAuthFlow() {
255
256
  console.log(' Opening browser to sign in...');
256
257
  console.log(` If the browser didn't open, visit: ${authUrl}`);
257
258
  console.log('');
258
- // Open browser using platform-specific commands (no external deps)
259
- const { exec } = await import('node:child_process');
260
- const cmd = process.platform === 'win32' ? `start "" "${authUrl}"`
261
- : process.platform === 'darwin' ? `open "${authUrl}"`
262
- : `xdg-open "${authUrl}"`;
263
- exec(cmd, () => { });
259
+ // Hardened: openBrowser uses execFile + argv array + URL protocol
260
+ // allowlist. Previous inline `exec(\`start "" "${authUrl}"\`)`
261
+ // would have shelled out any `&` / `|` / `^` / backtick / `$()` in
262
+ // a URL see src/open-browser.ts.
263
+ const { openBrowser } = await import('./open-browser.js');
264
+ try {
265
+ openBrowser(authUrl);
266
+ }
267
+ catch { /* non-fatal: user has the URL printed above */ }
264
268
  });
265
269
  server.on('error', (err) => {
266
270
  reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
@@ -429,7 +433,9 @@ async function exchangeCodeManual(code, codeVerifier, state) {
429
433
  });
430
434
  if (!res.ok) {
431
435
  const body = await res.text().catch(() => '');
432
- throw new Error(`Token exchange failed (HTTP ${res.status}): ${body.slice(0, 200)}`);
436
+ // See src/redact.ts strip tokens / JWTs / Bearer values from upstream
437
+ // body before they surface in the Error message.
438
+ throw new Error(`Token exchange failed (HTTP ${res.status}): ${redactSecrets(body.slice(0, 200))}`);
433
439
  }
434
440
  const data = await res.json();
435
441
  const tokens = {
@@ -490,7 +496,7 @@ async function doRefreshTokens() {
490
496
  });
491
497
  if (!res.ok) {
492
498
  const errBody = await res.text().catch(() => '');
493
- console.error(`[dario] Refresh attempt ${attempt + 1}/3 failed: HTTP ${res.status} — ${errBody.slice(0, 200)}`);
499
+ console.error(`[dario] Refresh attempt ${attempt + 1}/3 failed: HTTP ${res.status} — ${redactSecrets(errBody.slice(0, 200))}`);
494
500
  if (res.status === 401 || res.status === 403) {
495
501
  throw new Error(`Refresh token rejected (${res.status}). Run \`dario login\` to re-authenticate.`);
496
502
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Safe URL → default-browser dispatch.
3
+ *
4
+ * Replaces the inline `child_process.exec` template-string patterns that
5
+ * previously lived in `src/oauth.ts` and `src/accounts.ts`. Those used
6
+ * shell interpolation of an external URL (`exec(\`start "" "${url}"\`)`,
7
+ * `exec(\`xdg-open "${url}"\`)`) — defense-in-depth concern: a malicious
8
+ * `DARIO_OAUTH_AUTHORIZE_URL` override, a backdoored `claude` binary
9
+ * smuggling a different `CLAUDE_AI_AUTHORIZE_URL` literal through the
10
+ * cc-oauth-detect scanner, or any future code path that lets a less-
11
+ * trusted source reach this function would inject shell metacharacters
12
+ * (`&`, `|`, `>`, `^`, ``$()``, backtick) and execute arbitrary commands.
13
+ *
14
+ * Hardened path:
15
+ * 1. Parse the URL with WHATWG `URL` — throws on malformed input.
16
+ * 2. Allow only `http:` and `https:` (rejects `file:`, `javascript:`,
17
+ * `vbscript:`, `data:`, custom schemes that route through
18
+ * registered URL handlers).
19
+ * 3. Re-serialize via `parsed.toString()` so any pre-parse oddities
20
+ * get normalized through the URL spec.
21
+ * 4. Spawn the platform's URL-handler binary directly with the URL as
22
+ * a single argv element — no shell, no template interpolation.
23
+ * Windows uses `explorer.exe` (System32 binary, accepts URLs as
24
+ * argv, no cmd hop) instead of `cmd /c start`, which would parse
25
+ * `&`/`|` as command separators after Node's argv → cmd quoting.
26
+ * 5. Errors from the spawned process are swallowed: a failed browser
27
+ * open is non-fatal because every caller also prints the URL to
28
+ * stdout for manual paste.
29
+ *
30
+ * Tests use the `exec` option to inject a stub.
31
+ */
32
+ import { type ExecFileException } from 'node:child_process';
33
+ export interface OpenBrowserOptions {
34
+ /** Test hook — defaults to node:child_process execFile. */
35
+ exec?: (file: string, args: readonly string[], callback: (error: ExecFileException | null, stdout: string, stderr: string) => void) => void;
36
+ }
37
+ /**
38
+ * Build the (binary, argv) pair the current platform uses to dispatch a
39
+ * URL to its default browser. Exported for tests.
40
+ */
41
+ export declare function browserDispatchCommand(url: string, platform?: NodeJS.Platform): {
42
+ bin: string;
43
+ args: string[];
44
+ };
45
+ /**
46
+ * Open `url` in the user's default browser. See module docstring.
47
+ *
48
+ * @throws if the URL is malformed or has a protocol other than http/https.
49
+ * Browser-launch failures (handler missing, etc.) are swallowed —
50
+ * every caller already prints the URL for manual paste.
51
+ */
52
+ export declare function openBrowser(url: string, opts?: OpenBrowserOptions): void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Safe URL → default-browser dispatch.
3
+ *
4
+ * Replaces the inline `child_process.exec` template-string patterns that
5
+ * previously lived in `src/oauth.ts` and `src/accounts.ts`. Those used
6
+ * shell interpolation of an external URL (`exec(\`start "" "${url}"\`)`,
7
+ * `exec(\`xdg-open "${url}"\`)`) — defense-in-depth concern: a malicious
8
+ * `DARIO_OAUTH_AUTHORIZE_URL` override, a backdoored `claude` binary
9
+ * smuggling a different `CLAUDE_AI_AUTHORIZE_URL` literal through the
10
+ * cc-oauth-detect scanner, or any future code path that lets a less-
11
+ * trusted source reach this function would inject shell metacharacters
12
+ * (`&`, `|`, `>`, `^`, ``$()``, backtick) and execute arbitrary commands.
13
+ *
14
+ * Hardened path:
15
+ * 1. Parse the URL with WHATWG `URL` — throws on malformed input.
16
+ * 2. Allow only `http:` and `https:` (rejects `file:`, `javascript:`,
17
+ * `vbscript:`, `data:`, custom schemes that route through
18
+ * registered URL handlers).
19
+ * 3. Re-serialize via `parsed.toString()` so any pre-parse oddities
20
+ * get normalized through the URL spec.
21
+ * 4. Spawn the platform's URL-handler binary directly with the URL as
22
+ * a single argv element — no shell, no template interpolation.
23
+ * Windows uses `explorer.exe` (System32 binary, accepts URLs as
24
+ * argv, no cmd hop) instead of `cmd /c start`, which would parse
25
+ * `&`/`|` as command separators after Node's argv → cmd quoting.
26
+ * 5. Errors from the spawned process are swallowed: a failed browser
27
+ * open is non-fatal because every caller also prints the URL to
28
+ * stdout for manual paste.
29
+ *
30
+ * Tests use the `exec` option to inject a stub.
31
+ */
32
+ import { execFile } from 'node:child_process';
33
+ const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
34
+ /**
35
+ * Build the (binary, argv) pair the current platform uses to dispatch a
36
+ * URL to its default browser. Exported for tests.
37
+ */
38
+ export function browserDispatchCommand(url, platform = process.platform) {
39
+ const parsed = new URL(url);
40
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
41
+ throw new Error(`openBrowser: refusing to open URL with protocol "${parsed.protocol}" (only http/https allowed)`);
42
+ }
43
+ const safe = parsed.toString();
44
+ if (platform === 'win32') {
45
+ // explorer.exe accepts a URL as a single argv element and routes it
46
+ // through the registered URL handler. Avoids `cmd /c start "" "URL"`,
47
+ // which re-parses cmd metacharacters even when called via execFile
48
+ // because the cmd builtin runs *inside* cmd's parser, not Node's.
49
+ return { bin: 'explorer.exe', args: [safe] };
50
+ }
51
+ if (platform === 'darwin') {
52
+ return { bin: 'open', args: [safe] };
53
+ }
54
+ // Linux / BSD / other Unix.
55
+ return { bin: 'xdg-open', args: [safe] };
56
+ }
57
+ /**
58
+ * Open `url` in the user's default browser. See module docstring.
59
+ *
60
+ * @throws if the URL is malformed or has a protocol other than http/https.
61
+ * Browser-launch failures (handler missing, etc.) are swallowed —
62
+ * every caller already prints the URL for manual paste.
63
+ */
64
+ export function openBrowser(url, opts = {}) {
65
+ const { bin, args } = browserDispatchCommand(url);
66
+ const exec = opts.exec ?? execFile;
67
+ exec(bin, args, () => { });
68
+ }
package/dist/proxy.js CHANGED
@@ -13,6 +13,7 @@ import { Analytics, billingBucketFromClaim } from './analytics.js';
13
13
  import { loadAllAccounts, loadAccount, refreshAccountToken } from './accounts.js';
14
14
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
15
15
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
16
+ import { redactSecrets } from './redact.js';
16
17
  const ANTHROPIC_API = 'https://api.anthropic.com';
17
18
  const DEFAULT_PORT = 3456;
18
19
  const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
@@ -345,12 +346,10 @@ function translateStreamChunk(line) {
345
346
  }
346
347
  const OPENAI_MODELS_LIST = { object: 'list', data: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'].map(id => ({ id, object: 'model', created: 1700000000, owned_by: 'anthropic' })) };
347
348
  export function sanitizeError(err) {
348
- const msg = err instanceof Error ? err.message : String(err);
349
- // Never leak tokens, JWTs, or bearer values in error messages
350
- return msg
351
- .replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]')
352
- .replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
353
- .replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]');
349
+ // Pattern set lives in src/redact.ts so OAuth call sites can run the
350
+ // same redaction directly on response-body strings without importing
351
+ // proxy (which imports oauth — would circle).
352
+ return redactSecrets(err instanceof Error ? err.message : String(err));
354
353
  }
355
354
  /**
356
355
  * API-key auth via DARIO_API_KEY (x-api-key or Authorization: Bearer).
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Secret redaction for free-form strings emitted by dario.
3
+ *
4
+ * Used wherever an externally-sourced string (an upstream HTTP response
5
+ * body, an exception message, a verbose log line) might transit through
6
+ * to a place a user can see (stderr, an Error message, a JSON response).
7
+ * Even though Anthropic's documented API is not known to echo tokens in
8
+ * error responses, defense-in-depth: a future API change, a CDN error
9
+ * page that captures the request headers, or an intermediary's debug
10
+ * dump could surface a token, and we'd rather redact in transit than
11
+ * audit every call site for novel leak shapes.
12
+ *
13
+ * Patterns match formats actually seen in the Anthropic ecosystem:
14
+ * - `sk-ant-…` — long-lived API keys
15
+ * - JWT triple — OAuth access tokens (`eyJhdr.eyJpyld.sig`)
16
+ * - `Bearer <…>` — auth headers, raw or quoted
17
+ *
18
+ * Re-exported by `proxy.ts:sanitizeError` so the proxy's existing leak-
19
+ * shield benefits from any new patterns added here, and consumed
20
+ * directly by the OAuth code paths in `oauth.ts` / `accounts.ts` for
21
+ * sanitizing upstream error bodies before they hit `throw new Error`.
22
+ */
23
+ export declare const SECRET_PATTERNS: ReadonlyArray<readonly [RegExp, string]>;
24
+ /**
25
+ * Apply every redaction pattern to a string. Idempotent — running a
26
+ * pre-redacted string through this is a no-op.
27
+ */
28
+ export declare function redactSecrets(s: string): string;
package/dist/redact.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Secret redaction for free-form strings emitted by dario.
3
+ *
4
+ * Used wherever an externally-sourced string (an upstream HTTP response
5
+ * body, an exception message, a verbose log line) might transit through
6
+ * to a place a user can see (stderr, an Error message, a JSON response).
7
+ * Even though Anthropic's documented API is not known to echo tokens in
8
+ * error responses, defense-in-depth: a future API change, a CDN error
9
+ * page that captures the request headers, or an intermediary's debug
10
+ * dump could surface a token, and we'd rather redact in transit than
11
+ * audit every call site for novel leak shapes.
12
+ *
13
+ * Patterns match formats actually seen in the Anthropic ecosystem:
14
+ * - `sk-ant-…` — long-lived API keys
15
+ * - JWT triple — OAuth access tokens (`eyJhdr.eyJpyld.sig`)
16
+ * - `Bearer <…>` — auth headers, raw or quoted
17
+ *
18
+ * Re-exported by `proxy.ts:sanitizeError` so the proxy's existing leak-
19
+ * shield benefits from any new patterns added here, and consumed
20
+ * directly by the OAuth code paths in `oauth.ts` / `accounts.ts` for
21
+ * sanitizing upstream error bodies before they hit `throw new Error`.
22
+ */
23
+ export const SECRET_PATTERNS = [
24
+ [/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]'],
25
+ [/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]'],
26
+ [/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]'],
27
+ ];
28
+ /**
29
+ * Apply every redaction pattern to a string. Idempotent — running a
30
+ * pre-redacted string through this is a no-op.
31
+ */
32
+ export function redactSecrets(s) {
33
+ let out = s;
34
+ for (const [pat, repl] of SECRET_PATTERNS) {
35
+ out = out.replace(pat, repl);
36
+ }
37
+ return out;
38
+ }
@@ -50,6 +50,16 @@ export interface RequestQueueOptions {
50
50
  maxConcurrent?: number;
51
51
  maxQueued?: number;
52
52
  queueTimeoutMs?: number;
53
+ /**
54
+ * Whether timeout timers are `unref`'d so they don't by themselves keep
55
+ * the Node event loop alive. Default `true` — appropriate for the proxy,
56
+ * where a leaked queue entry should never hang shutdown. Pass `false` in
57
+ * tests where the queue is the only pending work on the loop: an
58
+ * `unref`'d timer won't fire in that case (Node exits with "unsettled
59
+ * top-level await" before the 50ms timeout elapses), so the reject the
60
+ * test is waiting for never arrives.
61
+ */
62
+ unrefTimers?: boolean;
53
63
  }
54
64
  export declare const DEFAULT_MAX_CONCURRENT = 10;
55
65
  export declare const DEFAULT_MAX_QUEUED = 128;
@@ -58,6 +68,7 @@ export declare class RequestQueue {
58
68
  readonly maxConcurrent: number;
59
69
  readonly maxQueued: number;
60
70
  readonly queueTimeoutMs: number;
71
+ readonly unrefTimers: boolean;
61
72
  private active;
62
73
  private queue;
63
74
  constructor(opts?: RequestQueueOptions);
@@ -47,12 +47,14 @@ export class RequestQueue {
47
47
  maxConcurrent;
48
48
  maxQueued;
49
49
  queueTimeoutMs;
50
+ unrefTimers;
50
51
  active = 0;
51
52
  queue = [];
52
53
  constructor(opts = {}) {
53
54
  this.maxConcurrent = opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
54
55
  this.maxQueued = opts.maxQueued ?? DEFAULT_MAX_QUEUED;
55
56
  this.queueTimeoutMs = opts.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS;
57
+ this.unrefTimers = opts.unrefTimers ?? true;
56
58
  }
57
59
  /**
58
60
  * Acquire a concurrency slot. Resolves when admitted; throws
@@ -80,7 +82,9 @@ export class RequestQueue {
80
82
  }, this.queueTimeoutMs);
81
83
  // Keep the timer from pinning the event loop open on shutdown. A queued
82
84
  // request waiting for a slot shouldn't by itself keep the process alive.
83
- timeoutHandle.unref?.();
85
+ // Opt-out for tests — see `unrefTimers` comment in RequestQueueOptions.
86
+ if (this.unrefTimers)
87
+ timeoutHandle.unref?.();
84
88
  const entry = { resolve, reject, enqueuedAt, timeoutHandle };
85
89
  this.queue.push(entry);
86
90
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.31.14",
3
+ "version": "3.31.16",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "start": "node dist/cli.js",
29
29
  "dev": "tsx src/cli.ts",
30
30
  "e2e": "node test/e2e.mjs",
31
+ "stress": "node test/stress.mjs",
31
32
  "compat": "node test/compat.mjs",
32
33
  "lint:pkg": "node scripts/check-package-json.mjs",
33
34
  "drift:sdk": "node scripts/check-sdk-drift.mjs",