@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 +4 -1
- package/dist/config-report.js +7 -0
- package/dist/doctor.js +24 -1
- package/dist/oauth.d.ts +33 -0
- package/dist/oauth.js +50 -4
- package/dist/proxy.js +3 -1
- package/package.json +1 -1
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 · ~
|
|
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
|
|
package/dist/config-report.js
CHANGED
|
@@ -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('; ')} —
|
|
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
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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.
|
|
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": {
|