@askalf/dario 3.31.13 → 3.31.14

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
@@ -202,6 +202,8 @@ dario accounts list
202
202
  dario proxy
203
203
  ```
204
204
 
205
+ If you already have a single-account `dario login` set up and run `dario accounts add <alias>` for the first time, dario **back-fills** your existing login credentials into the pool under the reserved alias `login` before running OAuth for the new alias. Net effect: your first `accounts add` gives you two pool accounts (login + new alias), pool mode activates immediately. Back-fill is one-shot, idempotent, and never touches your existing `credentials.json` — if you later `dario accounts remove` below the 2+ threshold, single-account mode reads it unchanged. Skipped if you explicitly pick `login` as the new alias — your intent wins.
206
+
205
207
  Each request picks the account with the highest headroom:
206
208
 
207
209
  ```
@@ -340,7 +342,7 @@ A version marker (`<!-- dario-sub-agent-version: X -->`) embedded in the markdow
340
342
  | `dario status` | Show Claude backend OAuth token health and expiry |
341
343
  | `dario refresh` | Force an immediate Claude token refresh |
342
344
  | `dario logout` | Delete stored Claude credentials |
343
- | `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management |
345
+ | `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management. `add <alias>` on a fresh pool auto back-fills your existing `dario login` credentials as `login`, so your first `add` trips the 2+ pool threshold on its own — see [Multi-account pool mode](#multi-account-pool-mode). |
344
346
  | `dario backend list` / `add <name> --key=<key> [--base-url=<url>]` / `remove <name>` | OpenAI-compat backend management |
345
347
  | `dario shim -- <cmd> [args...]` | Run a child process with the in-process fetch patch (see [Shim mode](#shim-mode)) |
346
348
  | `dario subagent install` / `remove` / `status` | CC sub-agent lifecycle (v3.26 — see [sub-agent hook](#claude-code-sub-agent-hook-v326)) |
@@ -762,6 +764,17 @@ Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter,
762
764
  **Something's wrong. Where do I start?**
763
765
  `dario doctor`. One command, one aggregated report — dario version, Node, platform, runtime/TLS classification, CC binary compat, template source + age + drift, OAuth status, pool state, backends, sub-agent install state, home dir. Exit code 1 if any check fails. Paste the output when you file an issue. (If you're inside Claude Code, `dario subagent install` once and then ask CC to "use the dario sub-agent to run doctor" — same output, no context switch.)
764
766
 
767
+ **OpenClaw returns 401 after I set `DARIO_API_KEY` (or upgrade past v3.30.6).**
768
+ If you run `dario proxy --host=0.0.0.0` (non-loopback), dario requires `DARIO_API_KEY` to be set so it's not an open subscription relay. OpenClaw 2026.2.17+ prefers `~/.openclaw/agents/main/agent/auth-profiles.json` over `openclaw.json`'s `apiKey` field or the `ANTHROPIC_API_KEY` env var — so if you have a stale Anthropic token in `auth-profiles.json` from an earlier setup, OpenClaw sends *that* token instead of `dario`, and dario rejects the request with `Authorization present but value mismatch` (visible under `dario proxy -v`, added in v3.31.2).
769
+
770
+ Three fixes, in order of simplicity:
771
+
772
+ 1. **Use loopback.** `dario proxy --host=127.0.0.1` — auth only enforced on non-loopback binds, no `DARIO_API_KEY` required, no OpenClaw changes. Best if you don't actually need LAN reach to dario.
773
+ 2. **Delete the Anthropic auth profile.** Remove the `"anthropic:default"` entry from `~/.openclaw/agents/main/agent/auth-profiles.json`. OpenClaw then falls back through the config chain and picks up `ANTHROPIC_API_KEY=dario` from the env. Confirmed working by [@tetsuco in #97](https://github.com/askalf/dario/issues/97).
774
+ 3. **Overwrite the auth profile.** `openclaw models auth paste-token --provider anthropic` and paste `dario`. Replaces whatever key was in there — keep a backup if you use it elsewhere.
775
+
776
+ Diagnose with `dario proxy -v` — the reject log (v3.31.2+) reports header-name only (never the value, since it may be a real credential you mistyped) and tells you which of the three configs is actually being hit.
777
+
765
778
  **What happens when Anthropic rotates the OAuth config?**
766
779
  Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v6.json`, keyed by the CC binary fingerprint. The cache path version bumps each time the canonical OAuth config shape changes so stale caches regenerate automatically on upgrade — v3 → v4 in v3.19.4 (scope-list flip CC v2.1.104 → v2.1.107), v4 → v5 in v3.31.3 (authorize URL `claude.com/cai/` → `claude.ai/` host normalization), v5 → v6 in v3.31.4 (6-scope restore after CC v2.1.116).
767
780
 
@@ -781,6 +794,9 @@ Env vars win over the file. Set `DARIO_OAUTH_DISABLE_OVERRIDE=1` to force pure a
781
794
  **What happens when Anthropic changes the CC request template?**
782
795
  Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
783
796
 
797
+ **Why does `dario accounts list` show an account called `login` I never added?**
798
+ That's your existing `dario login` credentials, back-filled into the pool automatically on your first `dario accounts add <alias>`. Pool mode activates at 2+ accounts in `~/.dario/accounts/`, and the single-account `credentials.json` store lives outside that directory — so without the back-fill, one `accounts add` would leave you at 1 pool entry and your login account orphaned. The `login` alias is reserved for this path. Safe to `dario accounts remove login` if you don't want it pooled; the original `credentials.json` is untouched by the back-fill, so single-account mode resumes reading it after removal drops you below the 2+ threshold. See [Multi-account pool mode](#multi-account-pool-mode) for the full picture.
799
+
784
800
  **First time setup on a fresh Claude account.**
785
801
  If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
786
802
  ```bash
@@ -23,3 +23,37 @@ export declare function _accountRefreshesInFlightSizeForTest(): number;
23
23
  */
24
24
  export declare function addAccountViaOAuth(alias: string): Promise<AccountCredentials>;
25
25
  export declare function getAccountsDir(): string;
26
+ /**
27
+ * Alias reserved for credentials auto-migrated from the single-account
28
+ * `dario login` store. Named `login` so it's semantically obvious where
29
+ * the entry came from and unlikely to collide with user-chosen aliases
30
+ * like `work`, `personal`, etc. If a user specifically requests `login`
31
+ * as the alias for `dario accounts add`, the caller falls back to
32
+ * `default` so the migration doesn't step on the user's intent.
33
+ */
34
+ export declare const MIGRATED_LOGIN_ALIAS = "login";
35
+ /**
36
+ * Promote the user's existing single-account `dario login` credentials
37
+ * (`~/.dario/credentials.json`, `~/.claude/.credentials.json`, or OS
38
+ * keychain — whichever `loadCredentials` finds) into the pool under a
39
+ * reserved alias.
40
+ *
41
+ * Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
42
+ * A user with one `dario login` account + one `dario accounts add bar`
43
+ * ends up with only one account in `accounts/` (bar), pool mode never
44
+ * trips, and the login account is effectively orphaned while pool is off.
45
+ * Calling this on the first `dario accounts add` back-fills the login
46
+ * account into the pool so the second `add` crosses the threshold.
47
+ *
48
+ * Idempotent: no-op if `accounts/` already has any entry, no-op if no
49
+ * credentials are reachable anywhere. Returns the alias written to, or
50
+ * `null` when nothing happened.
51
+ *
52
+ * The source `credentials.json` (if present) is left untouched — single-
53
+ * account mode still reads it if the user later `accounts remove`s down
54
+ * below the pool threshold. Migration is copy-only, never destructive.
55
+ *
56
+ * @param preferredAlias caller may request a specific alias. If it's
57
+ * already the reserved `login` (or collides), falls back to `default`.
58
+ */
59
+ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<string | null>;
package/dist/accounts.js CHANGED
@@ -2,10 +2,15 @@
2
2
  * Multi-account credential storage.
3
3
  *
4
4
  * Accounts live at `~/.dario/accounts/<alias>.json`. Single-account dario
5
- * still uses `~/.dario/credentials.json` and does not touch this module.
6
- * When `~/.dario/accounts/` contains 2+ files the proxy activates pool mode
7
- * (see pool.ts). Each account has its own independent OAuth lifecycle and
8
- * can refresh without affecting the others.
5
+ * uses `~/.dario/credentials.json` (plus the CC file + OS keychain fallback
6
+ * paths in oauth.ts). When `~/.dario/accounts/` contains 2+ files the proxy
7
+ * activates pool mode (see pool.ts). Each account has its own independent
8
+ * OAuth lifecycle and can refresh without affecting the others.
9
+ *
10
+ * `ensureLoginCredentialsInPool` (below) bridges the two stores on the
11
+ * first `dario accounts add` — it promotes the user's existing login
12
+ * credentials into the pool under a reserved alias so that adding a
13
+ * second account actually trips the 2+ threshold and activates pooling.
9
14
  *
10
15
  * OAuth config (client_id, scopes, authorize URL, token URL) comes from
11
16
  * dario's cc-oauth-detect scanner — the same source the single-account
@@ -17,6 +22,7 @@ import { homedir } from 'node:os';
17
22
  import { randomUUID, randomBytes, createHash } from 'node:crypto';
18
23
  import { createServer } from 'node:http';
19
24
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
25
+ import { loadCredentials } from './oauth.js';
20
26
  const DARIO_DIR = join(homedir(), '.dario');
21
27
  const ACCOUNTS_DIR = join(DARIO_DIR, 'accounts');
22
28
  /**
@@ -308,3 +314,61 @@ export async function addAccountViaOAuth(alias) {
308
314
  export function getAccountsDir() {
309
315
  return ACCOUNTS_DIR;
310
316
  }
317
+ /**
318
+ * Alias reserved for credentials auto-migrated from the single-account
319
+ * `dario login` store. Named `login` so it's semantically obvious where
320
+ * the entry came from and unlikely to collide with user-chosen aliases
321
+ * like `work`, `personal`, etc. If a user specifically requests `login`
322
+ * as the alias for `dario accounts add`, the caller falls back to
323
+ * `default` so the migration doesn't step on the user's intent.
324
+ */
325
+ export const MIGRATED_LOGIN_ALIAS = 'login';
326
+ /**
327
+ * Promote the user's existing single-account `dario login` credentials
328
+ * (`~/.dario/credentials.json`, `~/.claude/.credentials.json`, or OS
329
+ * keychain — whichever `loadCredentials` finds) into the pool under a
330
+ * reserved alias.
331
+ *
332
+ * Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
333
+ * A user with one `dario login` account + one `dario accounts add bar`
334
+ * ends up with only one account in `accounts/` (bar), pool mode never
335
+ * trips, and the login account is effectively orphaned while pool is off.
336
+ * Calling this on the first `dario accounts add` back-fills the login
337
+ * account into the pool so the second `add` crosses the threshold.
338
+ *
339
+ * Idempotent: no-op if `accounts/` already has any entry, no-op if no
340
+ * credentials are reachable anywhere. Returns the alias written to, or
341
+ * `null` when nothing happened.
342
+ *
343
+ * The source `credentials.json` (if present) is left untouched — single-
344
+ * account mode still reads it if the user later `accounts remove`s down
345
+ * below the pool threshold. Migration is copy-only, never destructive.
346
+ *
347
+ * @param preferredAlias caller may request a specific alias. If it's
348
+ * already the reserved `login` (or collides), falls back to `default`.
349
+ */
350
+ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS) {
351
+ if (!safeAliasPath(alias))
352
+ return null;
353
+ const existing = await listAccountAliases();
354
+ if (existing.length > 0)
355
+ return null;
356
+ const creds = await loadCredentials();
357
+ const tok = creds?.claudeAiOauth;
358
+ if (!tok?.accessToken || !tok?.refreshToken)
359
+ return null;
360
+ const identity = (await detectClaudeIdentity()) ?? {
361
+ deviceId: randomUUID(),
362
+ accountUuid: randomUUID(),
363
+ };
364
+ await saveAccount({
365
+ alias,
366
+ accessToken: tok.accessToken,
367
+ refreshToken: tok.refreshToken,
368
+ expiresAt: tok.expiresAt,
369
+ scopes: tok.scopes ?? [],
370
+ deviceId: identity.deviceId,
371
+ accountUuid: identity.accountUuid,
372
+ });
373
+ return alias;
374
+ }
@@ -131,7 +131,11 @@ export function buildProbeAuthorizeUrl(cfg) {
131
131
  scope: cfg.scopes,
132
132
  code_challenge: pkceChallenge(),
133
133
  code_challenge_method: 'S256',
134
- state: base64url(randomBytes(16)),
134
+ // 32 bytes — match what CC v2.1.116+ actually sends. See dario#71.
135
+ // Shorter states produce "Invalid request format" from Anthropic's
136
+ // authorize endpoint, which the probe classifier would otherwise mis-
137
+ // attribute to drift when it's actually our own request shape.
138
+ state: base64url(randomBytes(32)),
135
139
  });
136
140
  return `${cfg.authorizeUrl}?${params.toString()}`;
137
141
  }
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import { homedir } from 'node:os';
38
38
  import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
39
39
  import { startProxy, sanitizeError } from './proxy.js';
40
40
  import { VALID_EFFORT_VALUES } from './cc-template.js';
41
- import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount } from './accounts.js';
41
+ import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
42
42
  import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
43
43
  const args = process.argv.slice(2);
44
44
  const command = args[0] ?? 'proxy';
@@ -463,6 +463,22 @@ async function accounts() {
463
463
  console.error(`[dario] Account "${alias}" already exists. Remove it first with \`dario accounts remove ${alias}\`.`);
464
464
  process.exit(1);
465
465
  }
466
+ // If the user has `dario login` credentials on disk or in the keychain
467
+ // and the pool is empty, migrate those credentials into the pool first.
468
+ // Otherwise the new account lives alone in accounts/, pool mode never
469
+ // trips the 2+ threshold, and the login account is orphaned from the
470
+ // pool until the user figures out they have to re-`accounts add` it.
471
+ // Skip silently when the user explicitly picks the reserved alias —
472
+ // their intent wins, they can run `accounts add` again for the login
473
+ // migration under a different alias.
474
+ if (existing.length === 0 && alias !== MIGRATED_LOGIN_ALIAS) {
475
+ const migrated = await ensureLoginCredentialsInPool();
476
+ if (migrated) {
477
+ console.log('');
478
+ console.log(` Migrated your existing \`dario login\` account into the pool as "${migrated}".`);
479
+ console.log(` (Pool mode activates on 2+ accounts — this back-fill plus "${alias}" crosses that.)`);
480
+ }
481
+ }
466
482
  console.log('');
467
483
  console.log(` Adding account "${alias}" to the pool...`);
468
484
  console.log('');
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { OAuthTokens, CredentialsFile } from './oauth.js';
9
9
  export { startProxy, sanitizeError } from './proxy.js';
10
10
  export { AccountPool, parseRateLimits } from './pool.js';
11
11
  export type { PoolAccount, PoolStatus, RateLimitSnapshot, AccountIdentity } from './pool.js';
12
- export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, getAccountsDir, } from './accounts.js';
12
+ export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS, getAccountsDir, } from './accounts.js';
13
13
  export type { AccountCredentials } from './accounts.js';
14
14
  export { Analytics } from './analytics.js';
15
15
  export type { RequestRecord, AnalyticsSummary } from './analytics.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { startProxy, sanitizeError } from './proxy.js';
10
10
  // contains 2+ accounts; see README for the progression from single-account
11
11
  // mode to pool mode).
12
12
  export { AccountPool, parseRateLimits } from './pool.js';
13
- export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, getAccountsDir, } from './accounts.js';
13
+ export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS, getAccountsDir, } from './accounts.js';
14
14
  export { Analytics } from './analytics.js';
15
15
  // Multi-provider backends (v3.6.0+). Secondary OpenAI-compat providers
16
16
  // (OpenAI, OpenRouter, Groq, local LiteLLM, etc.) configured via
package/dist/oauth.js CHANGED
@@ -198,7 +198,10 @@ async function saveCredentials(creds) {
198
198
  export async function startAutoOAuthFlow() {
199
199
  const { createServer } = await import('node:http');
200
200
  const { codeVerifier, codeChallenge } = generatePKCE();
201
- const state = base64url(randomBytes(16));
201
+ // 32 random bytes → 43-char base64url state. See dario#71 — Anthropic's
202
+ // authorize endpoint rejects shorter states with "Invalid request format";
203
+ // CC v2.1.116+ ships 32. Keep in lockstep with CC's entropy-per-state.
204
+ const state = base64url(randomBytes(32));
202
205
  return new Promise((resolve, reject) => {
203
206
  const server = createServer((req, res) => {
204
207
  const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
@@ -387,7 +390,8 @@ export function detectHeadlessEnvironment() {
387
390
  */
388
391
  export async function startManualOAuthFlow() {
389
392
  const { codeVerifier, codeChallenge } = generatePKCE();
390
- const state = base64url(randomBytes(16));
393
+ // 32 bytes — same reason as startAutoOAuthFlow. See dario#71.
394
+ const state = base64url(randomBytes(32));
391
395
  const cfg = await getOAuthConfig();
392
396
  const authUrl = buildManualAuthorizeUrl(cfg, codeChallenge, state);
393
397
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.31.13",
3
+ "version": "3.31.14",
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": {
@@ -69,7 +69,7 @@
69
69
  "node": ">=18.0.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@types/node": "^22.0.0",
72
+ "@types/node": "^25.6.0",
73
73
  "tsx": "^4.19.0",
74
74
  "typescript": "^5.7.0"
75
75
  }