@askalf/dario 5.4.23 → 5.4.25

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
@@ -18,7 +18,9 @@
18
18
 
19
19
  <p><strong>One local endpoint. Every AI tool you own. The subscription you already pay for.</strong></p>
20
20
 
21
- <sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~22k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
21
+ <sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~24k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
22
+
23
+ <sub>Part of <a href="#own-your-stack"><strong>Own Your Stack</strong></a> — 12 open tools for owning your AI infra: <a href="https://github.com/askalf/truecopy">truecopy</a> · <a href="https://github.com/askalf/strongroom">strongroom</a> · <a href="https://github.com/askalf/fieldpass">fieldpass</a> · <a href="https://github.com/askalf/plumbline">plumbline</a> · <a href="#own-your-stack">full family ↓</a></sub>
22
24
 
23
25
  </div>
24
26
 
@@ -353,6 +355,7 @@ dario is the routing layer of **[Own Your Stack](https://github.com/askalf)**
353
355
  - **[strongroom](https://github.com/askalf/strongroom)** — own your agent secrets
354
356
  - **[cordon](https://github.com/askalf/cordon)** — own your prompts
355
357
  - **[fieldpass](https://github.com/askalf/fieldpass)** — own your agent browser
358
+ - **[plumbline](https://github.com/askalf/plumbline)** — own your agent oversight
356
359
  - **[amnesia](https://github.com/askalf/amnesia)** — own your search
357
360
  - **[askalf](https://askalf.org)** — own your operation: the AI operation that runs Sprayberry Labs
358
361
 
@@ -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
package/dist/doctor.js CHANGED
@@ -20,6 +20,7 @@ import { CC_TEMPLATE, resolveSystemPrompt, } from './cc-template.js';
20
20
  import { describeTemplate, detectDrift, checkCCCompat, findInstalledCC, SUPPORTED_CC_RANGE, CURRENT_SCHEMA_VERSION, compareVersions, } from './live-fingerprint.js';
21
21
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
22
22
  import { runAuthorizeProbe } from './cc-authorize-probe.js';
23
+ import { MIGRATED_LOGIN_ALIAS } from './accounts.js';
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
25
  /**
25
26
  * Format a epoch timestamp reset time relative to the current time.
@@ -109,6 +110,7 @@ export function checkIdentityDrift(input) {
109
110
  }
110
111
  const aligned = [];
111
112
  const drifted = [];
113
+ const driftedAliases = [];
112
114
  for (const acc of poolAccounts) {
113
115
  const deviceMatch = acc.deviceId === live.deviceId;
114
116
  const acctMatch = acc.accountUuid === live.accountUuid;
@@ -118,6 +120,7 @@ export function checkIdentityDrift(input) {
118
120
  else {
119
121
  const which = !deviceMatch && !acctMatch ? 'both' : !deviceMatch ? 'deviceId' : 'accountUuid';
120
122
  drifted.push(`${acc.alias} (${which})`);
123
+ driftedAliases.push(acc.alias);
121
124
  }
122
125
  }
123
126
  if (drifted.length === 0) {
@@ -127,10 +130,30 @@ export function checkIdentityDrift(input) {
127
130
  detail: `${aligned.length}/${poolAccounts.length} pool account${poolAccounts.length === 1 ? '' : 's'} match ~/.claude.json (userID=${shortId(live.deviceId)})`,
128
131
  }];
129
132
  }
133
+ // The remedy is deliberately NOT `accounts add <alias>`. That command
134
+ // exits 1 on an alias that already exists ("Remove it first"), and its
135
+ // default path runs a full OAuth browser flow rather than re-snapshotting
136
+ // — so naming it here walked every drifted user into a dead end. The two
137
+ // alias kinds genuinely need different commands: the reserved login alias
138
+ // is back-filled from whatever credentials are current, so removing it is
139
+ // enough (it re-materializes on the next `dario login` / `dario proxy`),
140
+ // while a user-added alias has to be removed and re-added, which re-runs
141
+ // OAuth for that account by design.
142
+ const loginDrifted = driftedAliases.includes(MIGRATED_LOGIN_ALIAS);
143
+ const otherDrifted = driftedAliases.filter((a) => a !== MIGRATED_LOGIN_ALIAS);
144
+ const fixes = [];
145
+ if (loginDrifted) {
146
+ fixes.push(`\`dario accounts remove ${MIGRATED_LOGIN_ALIAS}\` — it re-materializes from your ` +
147
+ `current credentials on the next \`dario login\` / \`dario proxy\``);
148
+ }
149
+ if (otherDrifted.length > 0) {
150
+ fixes.push(`\`dario accounts remove <alias>\` then \`dario accounts add <alias>\` to re-snapshot` +
151
+ `${otherDrifted.length === 1 ? '' : ' (for each of them)'} — the add re-runs OAuth for that account`);
152
+ }
130
153
  return [{
131
154
  status: 'warn',
132
155
  label: 'Identity',
133
- detail: `${drifted.length}/${poolAccounts.length} pool account${poolAccounts.length === 1 ? '' : 's'} drifted from ~/.claude.json (live userID=${shortId(live.deviceId)}): ${drifted.join('; ')} — re-run \`dario accounts add <alias>\` to refresh the stored snapshot, or non-Haiku requests on the drifted account(s) will 401`,
156
+ detail: `${drifted.length}/${poolAccounts.length} pool account${poolAccounts.length === 1 ? '' : 's'} drifted from ~/.claude.json (live userID=${shortId(live.deviceId)}): ${drifted.join('; ')} — non-Haiku requests on the drifted account(s) will 401. Fix: ${fixes.join('; ')}`,
134
157
  }];
135
158
  }
136
159
  /**
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.23",
3
+ "version": "5.4.25",
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": {