@askalf/dario 5.4.24 → 5.4.26

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.
@@ -18,6 +18,7 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
18
18
  import { join, dirname } from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { homedir } from 'node:os';
21
+ import { ignoreCcCredentials } from './oauth.js';
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  /**
23
24
  * Collect the effective dario configuration the proxy would run with.
@@ -76,6 +77,12 @@ export async function collectEffectiveConfig() {
76
77
  rows: [
77
78
  { label: 'credentials', value: credsInfo },
78
79
  { label: 'path', value: credsPath },
80
+ {
81
+ label: 'DARIO_IGNORE_CC_CREDENTIALS',
82
+ value: ignoreCcCredentials()
83
+ ? "on — using ONLY dario's own credentials.json; Claude Code session token + keychain ignored (won't rotate a live `claude` session)"
84
+ : 'off — also reads ~/.claude/.credentials.json + OS keychain, picks freshest (can rotate a live `claude` session on the same machine)',
85
+ },
79
86
  ],
80
87
  });
81
88
  // ── Pool
@@ -354,7 +354,7 @@ export declare function detectDrift(t: TemplateData, installedOverride?: string
354
354
  */
355
355
  export declare const SUPPORTED_CC_RANGE: {
356
356
  readonly min: "1.0.0";
357
- readonly maxTested: "2.1.220";
357
+ readonly maxTested: "2.1.221";
358
358
  };
359
359
  /**
360
360
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -969,7 +969,7 @@ export function detectDrift(t, installedOverride) {
969
969
  */
970
970
  export const SUPPORTED_CC_RANGE = {
971
971
  min: '1.0.0',
972
- maxTested: '2.1.220',
972
+ maxTested: '2.1.221',
973
973
  };
974
974
  /**
975
975
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/dist/oauth.d.ts CHANGED
@@ -22,6 +22,39 @@ export interface OAuthTokens {
22
22
  export interface CredentialsFile {
23
23
  claudeAiOauth: OAuthTokens;
24
24
  }
25
+ /**
26
+ * Whether to ignore the interactive Claude Code session's credentials entirely
27
+ * — set via `DARIO_IGNORE_CC_CREDENTIALS` (1/true/yes/on). Exported for tests.
28
+ *
29
+ * Default (unset) keeps the historical behaviour: loadCredentials reads dario's
30
+ * own file, CC's `~/.claude/.credentials.json`, AND the OS keychain (where
31
+ * modern CC stores its token), then uses the FRESHEST. That auto-detection is
32
+ * convenient for a machine dedicated to dario, but it is exactly what breaks a
33
+ * live `claude` session running on the SAME machine/account: the moment CC's
34
+ * token is fresher (e.g. right after you log into CC), dario grabs that same
35
+ * token and refreshes it, Anthropic rotates it, and the interactive session
36
+ * still holding the old copy starts 401ing.
37
+ *
38
+ * With the flag set, dario uses ONLY its own `~/.dario/credentials.json` (from a
39
+ * prior `dario login`) and never touches the CC file or keychain — so it stays
40
+ * on the Max subscription ($0) while never rotating the interactive session's
41
+ * token. The API-key path (`ANTHROPIC_UPSTREAM_API_KEY`) also isolates, but
42
+ * bypasses OAuth/Max onto retail billing, defeating the point of dario.
43
+ */
44
+ export declare function ignoreCcCredentials(env?: NodeJS.ProcessEnv): boolean;
45
+ /**
46
+ * Which credential sources loadCredentials() consults, given whether the
47
+ * interactive Claude Code session's credentials should be ignored. Exported so
48
+ * the isolation guarantee is testable without real credentials on disk.
49
+ *
50
+ * `readKeychain` gates the OS keychain the same way as the CC file: the modern
51
+ * CC binary stores its OAuth token in the keychain, not on disk, so isolating
52
+ * from the interactive session means skipping BOTH.
53
+ */
54
+ export declare function credentialSourcePlan(ignoreCc: boolean): {
55
+ filePaths: string[];
56
+ readKeychain: boolean;
57
+ };
25
58
  /**
26
59
  * Information about a keychain entry surfaced for operator disambiguation
27
60
  * during `dario accounts add --from-keychain`.
package/dist/oauth.js CHANGED
@@ -75,6 +75,43 @@ function generatePKCE() {
75
75
  function getDarioCredentialsPath() {
76
76
  return join(homedir(), '.dario', 'credentials.json');
77
77
  }
78
+ /**
79
+ * Whether to ignore the interactive Claude Code session's credentials entirely
80
+ * — set via `DARIO_IGNORE_CC_CREDENTIALS` (1/true/yes/on). Exported for tests.
81
+ *
82
+ * Default (unset) keeps the historical behaviour: loadCredentials reads dario's
83
+ * own file, CC's `~/.claude/.credentials.json`, AND the OS keychain (where
84
+ * modern CC stores its token), then uses the FRESHEST. That auto-detection is
85
+ * convenient for a machine dedicated to dario, but it is exactly what breaks a
86
+ * live `claude` session running on the SAME machine/account: the moment CC's
87
+ * token is fresher (e.g. right after you log into CC), dario grabs that same
88
+ * token and refreshes it, Anthropic rotates it, and the interactive session
89
+ * still holding the old copy starts 401ing.
90
+ *
91
+ * With the flag set, dario uses ONLY its own `~/.dario/credentials.json` (from a
92
+ * prior `dario login`) and never touches the CC file or keychain — so it stays
93
+ * on the Max subscription ($0) while never rotating the interactive session's
94
+ * token. The API-key path (`ANTHROPIC_UPSTREAM_API_KEY`) also isolates, but
95
+ * bypasses OAuth/Max onto retail billing, defeating the point of dario.
96
+ */
97
+ export function ignoreCcCredentials(env = process.env) {
98
+ const v = (env['DARIO_IGNORE_CC_CREDENTIALS'] ?? '').trim().toLowerCase();
99
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
100
+ }
101
+ /**
102
+ * Which credential sources loadCredentials() consults, given whether the
103
+ * interactive Claude Code session's credentials should be ignored. Exported so
104
+ * the isolation guarantee is testable without real credentials on disk.
105
+ *
106
+ * `readKeychain` gates the OS keychain the same way as the CC file: the modern
107
+ * CC binary stores its OAuth token in the keychain, not on disk, so isolating
108
+ * from the interactive session means skipping BOTH.
109
+ */
110
+ export function credentialSourcePlan(ignoreCc) {
111
+ return ignoreCc
112
+ ? { filePaths: [getDarioCredentialsPath()], readKeychain: false }
113
+ : { filePaths: [getDarioCredentialsPath(), getClaudeCodeCredentialsPath()], readKeychain: true };
114
+ }
78
115
  function getClaudeCodeCredentialsPath() {
79
116
  return join(homedir(), '.claude', '.credentials.json');
80
117
  }
@@ -362,8 +399,15 @@ export async function loadCredentials() {
362
399
  // work the way it did before any `dario login` had ever run, while
363
400
  // still preferring dario's own file when both sources are equivalent
364
401
  // (dario file wins ties on expiresAt by being checked first).
402
+ //
403
+ // DARIO_IGNORE_CC_CREDENTIALS narrows the source set to dario's OWN file
404
+ // only — never the interactive CC session's token (its file OR the keychain).
405
+ // See ignoreCcCredentials() / credentialSourcePlan() for why: on a machine
406
+ // shared with a live `claude` session, grabbing + refreshing CC's token here
407
+ // rotates it out from under that session.
408
+ const plan = credentialSourcePlan(ignoreCcCredentials());
365
409
  const candidates = [];
366
- for (const path of [getDarioCredentialsPath(), getClaudeCodeCredentialsPath()]) {
410
+ for (const path of plan.filePaths) {
367
411
  try {
368
412
  const raw = await readFile(path, 'utf-8');
369
413
  const parsed = JSON.parse(raw);
@@ -374,9 +418,11 @@ export async function loadCredentials() {
374
418
  catch { /* try next */ }
375
419
  }
376
420
  // OS keychain (modern CC stores credentials here, not on disk).
377
- const keychainCreds = await loadKeychainCredentials();
378
- if (keychainCreds?.claudeAiOauth?.accessToken && keychainCreds?.claudeAiOauth?.refreshToken) {
379
- candidates.push(keychainCreds);
421
+ if (plan.readKeychain) {
422
+ const keychainCreds = await loadKeychainCredentials();
423
+ if (keychainCreds?.claudeAiOauth?.accessToken && keychainCreds?.claudeAiOauth?.refreshToken) {
424
+ candidates.push(keychainCreds);
425
+ }
380
426
  }
381
427
  const best = pickFreshestCredentials(candidates);
382
428
  if (!best)
package/dist/proxy.js CHANGED
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { setDefaultResultOrder } from 'node:dns';
7
7
  import { arch, platform } from 'node:process';
8
- import { getAccessToken, getStatus } from './oauth.js';
8
+ import { getAccessToken, getStatus, ignoreCcCredentials } from './oauth.js';
9
9
  import { buildHealthResponse, derivePoolStatus, shouldDiscloseHealthInternals } from './health-response.js';
10
10
  import { darioVersion } from './version.js';
11
11
  import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
@@ -962,6 +962,8 @@ export async function startProxy(opts = {}) {
962
962
  const upstreamApiKey = (opts.upstreamApiKey ?? process.env.ANTHROPIC_UPSTREAM_API_KEY ?? '').trim();
963
963
  if (upstreamApiKey)
964
964
  console.error('[dario] upstream auth: per-token API key (x-api-key) — OAuth/Max + account pool bypassed');
965
+ else if (ignoreCcCredentials())
966
+ console.error("[dario] DARIO_IGNORE_CC_CREDENTIALS: using ONLY dario's own credentials.json — Claude Code session token + keychain ignored (won't rotate a live `claude` session; run `dario login` if not authed)");
965
967
  // DNS result order — prefer IPv4 for the Anthropic upstream by default.
966
968
  // api.anthropic.com publishes both A and AAAA records. In a container with
967
969
  // no IPv6 egress (e.g. a default Docker bridge network), Node's `verbatim`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.4.24",
3
+ "version": "5.4.26",
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": {