@askalf/dario 5.4.20 → 5.4.22

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.
@@ -286,6 +286,8 @@ export declare function findInstalledCC(): {
286
286
  path: string | null;
287
287
  version: string | null;
288
288
  };
289
+ /** Test-only: drop the resolved-binary memo. */
290
+ export declare function _resetClaudeBinCacheForTest(): void;
289
291
  export declare function enumerateClaudeCandidates(): string[];
290
292
  /**
291
293
  * Given a captured /v1/messages request body, pull out the fields that
@@ -341,10 +341,20 @@ function quarantineCorruptCache(reason) {
341
341
  * other's partial writes. Exposed for tests via `_atomicWriteJsonForTest`.
342
342
  */
343
343
  function atomicWriteJson(targetPath, data) {
344
- mkdirSync(dirname(targetPath), { recursive: true });
344
+ // 0700: whichever code path creates ~/.dario first decides that directory's
345
+ // permissions, and this one runs at startup, before any credential write.
346
+ // Without a mode it lands 755 on Linux, which makes every non-0600 file inside
347
+ // world-readable. The credential paths already create their dirs 0700; this was
348
+ // the one that could get there first and did not.
349
+ mkdirSync(dirname(targetPath), { recursive: true, mode: 0o700 });
345
350
  const tmp = `${targetPath}.${process.pid}.tmp`;
346
351
  try {
347
- writeFileSync(tmp, JSON.stringify(data, null, 2));
352
+ // 0600: this file holds the UNSCRUBBED live capture. Verified on the Linux
353
+ // deployment to contain `# Environment` (cwd, OS, platform) and `gitStatus:`
354
+ // (branch, modified files, recent commits); structurally it can also carry
355
+ // `# claudeMd`, `# userEmail` and `# auto memory` — the scrub list exists
356
+ // because CC emits them. Credentials beside it are 0600; this was 0644.
357
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
348
358
  renameSync(tmp, targetPath);
349
359
  }
350
360
  catch (err) {
@@ -587,11 +597,32 @@ export function findInstalledCC() {
587
597
  const version = path ? probeInstalledCCVersion() : null;
588
598
  return { path, version };
589
599
  }
600
+ // Resolving the binary means enumerating candidates and, when there is more
601
+ // than one, SPAWNING each to compare versions. That is a subprocess per
602
+ // candidate per call, and there are four call sites (refresh, capture,
603
+ // findInstalledCC, drift). Profiling a proxy start showed ~720ms of blocking
604
+ // spawnSync, over half of a ~1270ms startup, from repeating this and the
605
+ // version probe. The installed binary cannot change mid-process, so resolve
606
+ // once. Keyed on the override so a test flipping DARIO_CLAUDE_BIN is not served
607
+ // a stale answer.
608
+ let _claudeBinCache = null;
609
+ /** Test-only: drop the resolved-binary memo. */
610
+ export function _resetClaudeBinCacheForTest() {
611
+ _claudeBinCache = null;
612
+ }
590
613
  function findClaudeBinary() {
591
614
  // Honor an explicit override first — useful for tests and for users on
592
615
  // non-standard installs.
593
- if (process.env.DARIO_CLAUDE_BIN)
594
- return process.env.DARIO_CLAUDE_BIN;
616
+ const override = process.env.DARIO_CLAUDE_BIN;
617
+ if (override)
618
+ return override;
619
+ if (_claudeBinCache && _claudeBinCache.key === '')
620
+ return _claudeBinCache.value;
621
+ const resolved = resolveClaudeBinaryUncached();
622
+ _claudeBinCache = { key: '', value: resolved };
623
+ return resolved;
624
+ }
625
+ function resolveClaudeBinaryUncached() {
595
626
  const candidates = enumerateClaudeCandidates();
596
627
  if (candidates.length === 0)
597
628
  return null;
package/dist/proxy.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
3
- import { execSync } from 'node:child_process';
4
3
  import { readFileSync, readdirSync, createWriteStream } from 'node:fs';
5
4
  import { join } from 'node:path';
6
5
  import { homedir } from 'node:os';
@@ -11,7 +10,7 @@ import { buildHealthResponse, derivePoolStatus, shouldDiscloseHealthInternals }
11
10
  import { darioVersion } from './version.js';
12
11
  import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
13
12
  import { stampCch, hasCchSeed } from './cch.js';
14
- import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
13
+ import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
15
14
  import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, reconcilePoolAccounts, resolvePoolStrategy } from './pool.js';
16
15
  import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS } from './analytics.js';
17
16
  import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
@@ -118,8 +117,15 @@ export function buildBillingTag(cliVersion, cch) {
118
117
  function detectCliVersion() {
119
118
  const templateVersion = CC_TEMPLATE._version || '2.1.100';
120
119
  try {
121
- const out = execSync('claude --version', { timeout: 5000, stdio: 'pipe' }).toString().trim();
122
- return out.match(/^([\d]+\.[\d]+\.[\d]+)/)?.[1] ?? templateVersion;
120
+ // Was its own execSync('claude --version'), which made this the THIRD
121
+ // synchronous spawn of a 259 MB binary during startup for one version
122
+ // string — findClaudeBinary probes candidates, probeInstalledCCVersion
123
+ // probes the winner, and this probed again. probeInstalledCCVersion is
124
+ // memoised per process and resolves the binary properly (honours
125
+ // DARIO_CLAUDE_BIN, picks the newest candidate) rather than trusting
126
+ // whatever bare `claude` the shell finds, so reusing it is both cheaper and
127
+ // more correct.
128
+ return probeInstalledCCVersion() ?? templateVersion;
123
129
  }
124
130
  catch {
125
131
  return templateVersion;
@@ -1082,7 +1088,13 @@ export async function startProxy(opts = {}) {
1082
1088
  let logFileStream = null;
1083
1089
  if (logFilePath) {
1084
1090
  try {
1085
- logFileStream = createWriteStream(logFilePath, { flags: 'a' });
1091
+ // 0600 to match everything else dario writes. Records are redacted metadata
1092
+ // rather than bodies (writeLogLine runs redactSecrets; -vv bodies go to
1093
+ // stdout), so this is defence-in-depth — but it still shows which account
1094
+ // served which request and when, and 0644 contradicted the convention every
1095
+ // credential path here follows. Mode applies to CREATION only, so an
1096
+ // existing log keeps its own mode, which is right for append.
1097
+ logFileStream = createWriteStream(logFilePath, { flags: 'a', mode: 0o600 });
1086
1098
  logFileStream.on('error', (err) => {
1087
1099
  console.error(`[dario] log-file write error: ${err.message} (logging disabled)`);
1088
1100
  logFileStream = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.4.20",
3
+ "version": "5.4.22",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {