@askalf/dario 3.37.10 → 3.37.12
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 +17 -0
- package/dist/accounts.d.ts +27 -0
- package/dist/accounts.js +72 -1
- package/dist/cli.js +103 -15
- package/dist/oauth.d.ts +50 -1
- package/dist/oauth.js +210 -7
- package/dist/proxy.js +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -344,3 +344,20 @@ npm run e2e # live proxy + OAuth (requires a working Claude backend)
|
|
|
344
344
|
## License
|
|
345
345
|
|
|
346
346
|
MIT — see [LICENSE](LICENSE) and [DISCLAIMER.md](DISCLAIMER.md).
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Also by askalf
|
|
351
|
+
|
|
352
|
+
| Project | What it does |
|
|
353
|
+
|---------|-------------|
|
|
354
|
+
| [arnie](https://github.com/askalf/arnie) | Portable IT troubleshooting companion. Networking, AD, Windows Update, package managers, log triage, hardware checks. |
|
|
355
|
+
| [brio](https://github.com/askalf/brio) | Capability layer for AI workloads — semantic cache, cost tiering, policy. Sits in front of any Anthropic-compat endpoint. |
|
|
356
|
+
| [browser-bridge](https://github.com/askalf/browser-bridge) | Stealth headless Chromium in a container. CDP on 9222 — Playwright/Puppeteer/MCP-compatible. |
|
|
357
|
+
| [claude-bridge](https://github.com/askalf/claude-bridge) | Bridge Claude Code sessions to Discord. |
|
|
358
|
+
| [deepdive](https://github.com/askalf/deepdive) | Local research agent. Plan → search → fetch → extract → synthesize. Cited answers. |
|
|
359
|
+
| [git-providers](https://github.com/askalf/git-providers) | Unified GitHub + GitLab + Bitbucket Cloud REST clients behind one GitProvider interface. Plus a 44-entry api-key-provider taxonomy. |
|
|
360
|
+
| [hands](https://github.com/askalf/hands) | Cross-platform computer-use agent. Mouse, keyboard, screen. |
|
|
361
|
+
| [install-kit](https://github.com/askalf/install-kit) | curl-pipe-bash template for self-hosted Docker apps. |
|
|
362
|
+
| [pgflex](https://github.com/askalf/pgflex) | One Postgres API. Two modes (real PG ↔ PGlite WASM). |
|
|
363
|
+
| [redisflex](https://github.com/askalf/redisflex) | One Redis API. Two modes (ioredis ↔ in-process). |
|
package/dist/accounts.d.ts
CHANGED
|
@@ -35,6 +35,33 @@ export declare function addAccountViaOAuth(alias: string): Promise<AccountCreden
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function addAccountViaManualOAuth(alias: string): Promise<AccountCredentials>;
|
|
37
37
|
export declare function getAccountsDir(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Error subclass for the keychain-import path so the CLI can render
|
|
40
|
+
* actionable guidance (list of candidates) without parsing message strings.
|
|
41
|
+
*/
|
|
42
|
+
export declare class KeychainImportError extends Error {
|
|
43
|
+
readonly kind: 'empty' | 'ambiguous' | 'no-match';
|
|
44
|
+
readonly candidates: string[];
|
|
45
|
+
constructor(message: string, kind: 'empty' | 'ambiguous' | 'no-match', candidates?: string[]);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Import a Claude Code keychain entry into the pool under `alias`. Skips
|
|
49
|
+
* the OAuth flow entirely — reuses tokens the user already authorised
|
|
50
|
+
* through Claude Code itself. See askalf/dario#237 for design rationale.
|
|
51
|
+
*
|
|
52
|
+
* Resolution rules:
|
|
53
|
+
* - 0 entries on this host → throws KeychainImportError(kind: 'empty')
|
|
54
|
+
* - 1 entry total → imports it; `target` argument ignored if supplied
|
|
55
|
+
* - 2+ entries + no target → throws KeychainImportError(kind: 'ambiguous',
|
|
56
|
+
* candidates: [<target1>, <target2>, ...]) so the CLI can list them
|
|
57
|
+
* - 2+ entries + target → imports the matching one, throws
|
|
58
|
+
* KeychainImportError(kind: 'no-match', candidates) if none match
|
|
59
|
+
*
|
|
60
|
+
* macOS currently only ever surfaces a single entry (see the comment in
|
|
61
|
+
* enumerateKeychainCredentials in oauth.ts). Linux + Windows enumerate
|
|
62
|
+
* all matching entries.
|
|
63
|
+
*/
|
|
64
|
+
export declare function addAccountFromKeychain(alias: string, target?: string): Promise<AccountCredentials>;
|
|
38
65
|
/**
|
|
39
66
|
* Alias reserved for credentials auto-migrated from the single-account
|
|
40
67
|
* `dario login` store. Named `login` so it's semantically obvious where
|
package/dist/accounts.js
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
23
23
|
import { createServer } from 'node:http';
|
|
24
24
|
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
25
|
-
import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin } from './oauth.js';
|
|
25
|
+
import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
|
|
26
26
|
import { openBrowser } from './open-browser.js';
|
|
27
27
|
import { redactSecrets } from './redact.js';
|
|
28
28
|
const MANUAL_REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
|
|
@@ -388,6 +388,77 @@ export async function addAccountViaManualOAuth(alias) {
|
|
|
388
388
|
export function getAccountsDir() {
|
|
389
389
|
return ACCOUNTS_DIR;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Error subclass for the keychain-import path so the CLI can render
|
|
393
|
+
* actionable guidance (list of candidates) without parsing message strings.
|
|
394
|
+
*/
|
|
395
|
+
export class KeychainImportError extends Error {
|
|
396
|
+
kind;
|
|
397
|
+
candidates;
|
|
398
|
+
constructor(message, kind, candidates = []) {
|
|
399
|
+
super(message);
|
|
400
|
+
this.kind = kind;
|
|
401
|
+
this.candidates = candidates;
|
|
402
|
+
this.name = 'KeychainImportError';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Import a Claude Code keychain entry into the pool under `alias`. Skips
|
|
407
|
+
* the OAuth flow entirely — reuses tokens the user already authorised
|
|
408
|
+
* through Claude Code itself. See askalf/dario#237 for design rationale.
|
|
409
|
+
*
|
|
410
|
+
* Resolution rules:
|
|
411
|
+
* - 0 entries on this host → throws KeychainImportError(kind: 'empty')
|
|
412
|
+
* - 1 entry total → imports it; `target` argument ignored if supplied
|
|
413
|
+
* - 2+ entries + no target → throws KeychainImportError(kind: 'ambiguous',
|
|
414
|
+
* candidates: [<target1>, <target2>, ...]) so the CLI can list them
|
|
415
|
+
* - 2+ entries + target → imports the matching one, throws
|
|
416
|
+
* KeychainImportError(kind: 'no-match', candidates) if none match
|
|
417
|
+
*
|
|
418
|
+
* macOS currently only ever surfaces a single entry (see the comment in
|
|
419
|
+
* enumerateKeychainCredentials in oauth.ts). Linux + Windows enumerate
|
|
420
|
+
* all matching entries.
|
|
421
|
+
*/
|
|
422
|
+
export async function addAccountFromKeychain(alias, target) {
|
|
423
|
+
const entries = await enumerateKeychainCredentials();
|
|
424
|
+
if (entries.length === 0) {
|
|
425
|
+
throw new KeychainImportError('No Claude Code keychain entries found on this host. Run `claude` (login flow) first, or use `dario accounts add ' + alias + '` to start a fresh OAuth.', 'empty');
|
|
426
|
+
}
|
|
427
|
+
let chosen;
|
|
428
|
+
if (target) {
|
|
429
|
+
chosen = entries.find(e => e.target === target);
|
|
430
|
+
if (!chosen) {
|
|
431
|
+
throw new KeychainImportError(`No keychain entry matches target "${target}".`, 'no-match', entries.map(e => e.target));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else if (entries.length === 1) {
|
|
435
|
+
chosen = entries[0];
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
throw new KeychainImportError(`Found ${entries.length} keychain entries — pick one with --from-keychain=<target>.`, 'ambiguous', entries.map(e => e.target));
|
|
439
|
+
}
|
|
440
|
+
const oauth = chosen.credentials.claudeAiOauth;
|
|
441
|
+
if (!oauth?.accessToken || !oauth?.refreshToken) {
|
|
442
|
+
throw new KeychainImportError(`Keychain entry "${chosen.target}" is missing accessToken/refreshToken — re-authenticate Claude Code.`, 'empty');
|
|
443
|
+
}
|
|
444
|
+
// Same identity preference as addAccountViaOAuth — prefer CC identity if
|
|
445
|
+
// installed; otherwise generate fresh IDs.
|
|
446
|
+
const identity = (await detectClaudeIdentity()) ?? {
|
|
447
|
+
deviceId: randomUUID(),
|
|
448
|
+
accountUuid: randomUUID(),
|
|
449
|
+
};
|
|
450
|
+
const creds = {
|
|
451
|
+
alias,
|
|
452
|
+
accessToken: oauth.accessToken,
|
|
453
|
+
refreshToken: oauth.refreshToken,
|
|
454
|
+
expiresAt: oauth.expiresAt,
|
|
455
|
+
scopes: oauth.scopes ?? ['user:inference'],
|
|
456
|
+
deviceId: identity.deviceId,
|
|
457
|
+
accountUuid: identity.accountUuid,
|
|
458
|
+
};
|
|
459
|
+
await saveAccount(creds);
|
|
460
|
+
return creds;
|
|
461
|
+
}
|
|
391
462
|
/**
|
|
392
463
|
* Alias reserved for credentials auto-migrated from the single-account
|
|
393
464
|
* `dario login` store. Named `login` so it's semantically obvious where
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
24
24
|
import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
|
|
25
25
|
import { startProxy, sanitizeError } from './proxy.js';
|
|
26
26
|
import { VALID_EFFORT_VALUES } from './cc-template.js';
|
|
27
|
-
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
27
|
+
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, addAccountFromKeychain, KeychainImportError, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
28
28
|
import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
|
|
29
29
|
import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-proxy.js';
|
|
30
30
|
// `args` / `command` at module scope — command handlers below close over
|
|
@@ -38,9 +38,24 @@ async function login() {
|
|
|
38
38
|
console.log(' ───────────────────');
|
|
39
39
|
console.log('');
|
|
40
40
|
const manualFlag = args.includes('--manual') || args.includes('--headless');
|
|
41
|
+
// --force-reauth skips the existing-credentials short-circuit entirely.
|
|
42
|
+
// Use when the refresh token is dead and you need a clean OAuth re-auth
|
|
43
|
+
// without manually deleting credentials.json first.
|
|
44
|
+
const forceReauth = args.includes('--force-reauth') || args.includes('--force');
|
|
45
|
+
// --no-proxy keeps `dario login` to its name — it just does auth, doesn't
|
|
46
|
+
// try to start the proxy as a side effect. Useful in containerised deploys
|
|
47
|
+
// where the proxy is the container's CMD and is already running. Implicitly
|
|
48
|
+
// set by --manual since manual flow is for headless / scripted contexts
|
|
49
|
+
// where proxy lifecycle is managed externally.
|
|
50
|
+
const noProxy = args.includes('--no-proxy') || manualFlag;
|
|
41
51
|
// Check for existing credentials (Claude Code or dario's own)
|
|
42
|
-
const creds = await loadCredentials();
|
|
52
|
+
const creds = forceReauth ? null : await loadCredentials();
|
|
43
53
|
if (creds?.claudeAiOauth?.accessToken && creds.claudeAiOauth.expiresAt > Date.now()) {
|
|
54
|
+
if (noProxy) {
|
|
55
|
+
console.log(' Found valid credentials. (--no-proxy / --manual: not starting proxy.)');
|
|
56
|
+
console.log('');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
44
59
|
console.log(' Found credentials. Starting proxy...');
|
|
45
60
|
console.log('');
|
|
46
61
|
await proxy();
|
|
@@ -67,6 +82,10 @@ async function login() {
|
|
|
67
82
|
console.log('');
|
|
68
83
|
}
|
|
69
84
|
}
|
|
85
|
+
else if (forceReauth) {
|
|
86
|
+
console.log(' --force-reauth: skipping credential detection, starting fresh OAuth flow...');
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
70
89
|
else {
|
|
71
90
|
console.log(' No Claude Code credentials found. Starting OAuth flow...');
|
|
72
91
|
console.log('');
|
|
@@ -90,7 +109,12 @@ async function login() {
|
|
|
90
109
|
console.log(' Login successful!');
|
|
91
110
|
console.log(` Token expires in ${expiresIn} minutes (auto-refreshes).`);
|
|
92
111
|
console.log('');
|
|
93
|
-
|
|
112
|
+
if (noProxy) {
|
|
113
|
+
console.log(' (--no-proxy / --manual: credentials saved, proxy not started.)');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(' Run `dario proxy` to start the API proxy.');
|
|
117
|
+
}
|
|
94
118
|
console.log('');
|
|
95
119
|
}
|
|
96
120
|
catch (err) {
|
|
@@ -150,8 +174,19 @@ async function logout() {
|
|
|
150
174
|
await unlink(path);
|
|
151
175
|
console.log('[dario] Credentials removed.');
|
|
152
176
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
177
|
+
catch (err) {
|
|
178
|
+
const code = err?.code;
|
|
179
|
+
if (code === 'ENOENT') {
|
|
180
|
+
console.log('[dario] No credentials found.');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Permission denied, EISDIR, EBUSY, etc — surface the real error so the
|
|
184
|
+
// operator can fix it. Previous catch-all silently lied with "No
|
|
185
|
+
// credentials found" even when the file was clearly there but unreadable
|
|
186
|
+
// (e.g. ownership got mangled by a `docker run --user 0` recovery op).
|
|
187
|
+
console.error(`[dario] Could not remove ${path}: ${err.message}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
155
190
|
}
|
|
156
191
|
}
|
|
157
192
|
async function proxy() {
|
|
@@ -625,15 +660,30 @@ async function accounts() {
|
|
|
625
660
|
}
|
|
626
661
|
}
|
|
627
662
|
const manualAccountFlag = args.includes('--manual') || args.includes('--headless');
|
|
663
|
+
// --from-keychain[=<target>] imports an existing Claude Code keychain
|
|
664
|
+
// entry instead of running OAuth. Bare flag uses the only/first match;
|
|
665
|
+
// --from-keychain=<target> picks a specific entry by its platform-
|
|
666
|
+
// specific identifier (Linux account, Windows TargetName). See
|
|
667
|
+
// askalf/dario#237.
|
|
668
|
+
const keychainArg = args.find(a => a === '--from-keychain' || a === '--from-cc' || a.startsWith('--from-keychain=') || a.startsWith('--from-cc='));
|
|
669
|
+
const fromKeychain = keychainArg !== undefined;
|
|
670
|
+
const keychainTarget = keychainArg && keychainArg.includes('=') ? keychainArg.split('=', 2)[1] : undefined;
|
|
671
|
+
if (fromKeychain && manualAccountFlag) {
|
|
672
|
+
console.error('');
|
|
673
|
+
console.error(' --from-keychain and --manual are mutually exclusive (one skips OAuth, the other does it manually).');
|
|
674
|
+
console.error('');
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
628
677
|
console.log('');
|
|
629
|
-
console.log(` Adding account "${alias}" to the pool${manualAccountFlag ? ' (manual / headless flow)' : ''}...`);
|
|
678
|
+
console.log(` Adding account "${alias}" to the pool${manualAccountFlag ? ' (manual / headless flow)' : fromKeychain ? ' (importing from OS keychain)' : ''}...`);
|
|
630
679
|
console.log('');
|
|
631
680
|
// Mirror the heuristic that `dario login` uses: if the user didn't
|
|
632
681
|
// explicitly pick `--manual` AND we detect SSH / container / no-DISPLAY,
|
|
633
682
|
// print a hint before opening the browser. Doesn't auto-flip — false
|
|
634
683
|
// positives are more annoying than false negatives — but the hint keeps
|
|
635
|
-
// users from waiting for a browser redirect that can't land.
|
|
636
|
-
|
|
684
|
+
// users from waiting for a browser redirect that can't land. Skip the
|
|
685
|
+
// hint entirely when --from-keychain is set since no browser is opened.
|
|
686
|
+
if (!manualAccountFlag && !fromKeychain) {
|
|
637
687
|
const reason = detectHeadlessEnvironment();
|
|
638
688
|
if (reason) {
|
|
639
689
|
console.log(` Note: ${reason}. If the browser redirect doesn't land,`);
|
|
@@ -642,9 +692,11 @@ async function accounts() {
|
|
|
642
692
|
}
|
|
643
693
|
}
|
|
644
694
|
try {
|
|
645
|
-
const creds =
|
|
646
|
-
? await
|
|
647
|
-
:
|
|
695
|
+
const creds = fromKeychain
|
|
696
|
+
? await addAccountFromKeychain(alias, keychainTarget)
|
|
697
|
+
: manualAccountFlag
|
|
698
|
+
? await addAccountViaManualOAuth(alias)
|
|
699
|
+
: await addAccountViaOAuth(alias);
|
|
648
700
|
const minutes = Math.round((creds.expiresAt - Date.now()) / 60000);
|
|
649
701
|
console.log('');
|
|
650
702
|
console.log(` Account "${alias}" added.`);
|
|
@@ -662,6 +714,26 @@ async function accounts() {
|
|
|
662
714
|
console.log('');
|
|
663
715
|
}
|
|
664
716
|
catch (err) {
|
|
717
|
+
// KeychainImportError carries structured kind+candidates so we can
|
|
718
|
+
// render a targeted next step without parsing the message.
|
|
719
|
+
if (err instanceof KeychainImportError) {
|
|
720
|
+
console.error('');
|
|
721
|
+
console.error(` Failed to add account: ${err.message}`);
|
|
722
|
+
if (err.kind === 'ambiguous' && err.candidates.length > 0) {
|
|
723
|
+
console.error('');
|
|
724
|
+
console.error(' Available keychain entries:');
|
|
725
|
+
for (const t of err.candidates)
|
|
726
|
+
console.error(` --from-keychain="${t}"`);
|
|
727
|
+
}
|
|
728
|
+
else if (err.kind === 'no-match' && err.candidates.length > 0) {
|
|
729
|
+
console.error('');
|
|
730
|
+
console.error(' Available keychain entries:');
|
|
731
|
+
for (const t of err.candidates)
|
|
732
|
+
console.error(` --from-keychain="${t}"`);
|
|
733
|
+
}
|
|
734
|
+
console.error('');
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
665
737
|
const msg = sanitizeError(err);
|
|
666
738
|
console.error('');
|
|
667
739
|
console.error(` Failed to add account: ${msg}`);
|
|
@@ -669,7 +741,7 @@ async function accounts() {
|
|
|
669
741
|
// `dario login`. Auto flow can fail on EADDRINUSE (port already
|
|
670
742
|
// bound), SSH-tunnel mismatch, or the browser timing out before
|
|
671
743
|
// the user signs in. `--manual` works in all of those cases.
|
|
672
|
-
if (!manualAccountFlag && /callback server|EADDRINUSE|bind|timed out|did not receive/i.test(msg)) {
|
|
744
|
+
if (!manualAccountFlag && !fromKeychain && /callback server|EADDRINUSE|bind|timed out|did not receive/i.test(msg)) {
|
|
673
745
|
console.error(` Hint: try \`dario accounts add ${alias} --manual\` for headless / container setups.`);
|
|
674
746
|
}
|
|
675
747
|
console.error('');
|
|
@@ -796,22 +868,38 @@ async function help() {
|
|
|
796
868
|
dario — Use your Claude subscription as an API.
|
|
797
869
|
|
|
798
870
|
Usage:
|
|
799
|
-
dario login [--manual]
|
|
871
|
+
dario login [--manual] [--no-proxy] [--force-reauth]
|
|
872
|
+
Detect credentials + start proxy (or run OAuth).
|
|
800
873
|
--manual (alias: --headless) for container / SSH
|
|
801
874
|
setups — prints an authorize URL and reads the
|
|
802
|
-
code you paste back instead of a local redirect
|
|
875
|
+
code you paste back instead of a local redirect.
|
|
876
|
+
--no-proxy stops after auth — do not start the
|
|
877
|
+
proxy (implied by --manual). Use this when the
|
|
878
|
+
proxy is already running in a separate process /
|
|
879
|
+
container so login doesn't collide on the port.
|
|
880
|
+
--force-reauth (alias: --force) ignores any
|
|
881
|
+
existing credentials and runs a fresh OAuth
|
|
882
|
+
flow — for when the refresh token is dead and
|
|
883
|
+
/health still reports access-token countdown.
|
|
803
884
|
dario proxy [options] Start the API proxy server
|
|
804
885
|
dario status Check authentication status
|
|
805
886
|
dario refresh Force token refresh
|
|
806
887
|
dario logout Remove saved credentials
|
|
807
888
|
dario accounts list List accounts in the multi-account pool
|
|
808
|
-
dario accounts add NAME [--manual]
|
|
889
|
+
dario accounts add NAME [--manual] [--from-keychain[=<target>]]
|
|
809
890
|
Add a new account to the pool (runs OAuth flow).
|
|
810
891
|
--manual (alias: --headless) prints an authorize
|
|
811
892
|
URL and reads the code you paste back — for
|
|
812
893
|
container / SSH / no-browser-on-this-machine
|
|
813
894
|
setups, or as the on-Windows escape hatch when
|
|
814
895
|
the URL dispatch chain truncates query params.
|
|
896
|
+
--from-keychain skips OAuth and imports an
|
|
897
|
+
existing Claude Code keychain entry on this
|
|
898
|
+
host. With no value: uses the only/first match
|
|
899
|
+
(errors with the candidate list if multiple
|
|
900
|
+
entries exist). With =<target>: picks a specific
|
|
901
|
+
entry by its platform identifier (Linux account
|
|
902
|
+
attribute, Windows TargetName).
|
|
815
903
|
dario accounts remove N Remove an account from the pool
|
|
816
904
|
dario backend list List configured OpenAI-compat backends
|
|
817
905
|
dario backend add NAME --key=sk-... [--base-url=...]
|
package/dist/oauth.d.ts
CHANGED
|
@@ -22,6 +22,47 @@ export interface OAuthTokens {
|
|
|
22
22
|
export interface CredentialsFile {
|
|
23
23
|
claudeAiOauth: OAuthTokens;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Information about a keychain entry surfaced for operator disambiguation
|
|
27
|
+
* during `dario accounts add --from-keychain`.
|
|
28
|
+
*/
|
|
29
|
+
export interface KeychainEntry {
|
|
30
|
+
/**
|
|
31
|
+
* Implementation-defined identifier the operator uses to pick a specific
|
|
32
|
+
* entry. Stable per-platform but not equal across platforms:
|
|
33
|
+
* - Linux: the libsecret `account` attribute (or value when absent)
|
|
34
|
+
* - Windows: the Credential Manager TargetName (e.g.
|
|
35
|
+
* "Claude Code-credentials" or "Claude Code-credentials@<account-uuid>")
|
|
36
|
+
* - macOS: always "Claude Code-credentials" until macOS-side multi-entry
|
|
37
|
+
* enumeration is implemented (see comment in enumerateKeychainCredentials)
|
|
38
|
+
*/
|
|
39
|
+
target: string;
|
|
40
|
+
credentials: CredentialsFile;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Enumerate every Claude Code keychain entry on this host. Pool-mode
|
|
44
|
+
* counterpart to `loadKeychainCredentials` (which returns the first hit
|
|
45
|
+
* for the single-account login flow). Used by `dario accounts add
|
|
46
|
+
* --from-keychain` to import without rerunning OAuth.
|
|
47
|
+
*
|
|
48
|
+
* Per-platform coverage:
|
|
49
|
+
* - **Linux**: `secret-tool search --all service "Claude Code-credentials"`
|
|
50
|
+
* enumerates every matching attribute set. Account name comes from the
|
|
51
|
+
* `account` attribute when set, otherwise the secret hash truncated.
|
|
52
|
+
* - **Windows**: PowerShell + CredEnumerate already iterates every
|
|
53
|
+
* matching credential (existing pattern just wasn't exposing the
|
|
54
|
+
* TargetName). New script variant emits target + JSON blob per line.
|
|
55
|
+
* - **macOS**: returns at most one entry. The `security` CLI doesn't
|
|
56
|
+
* expose a clean enumeration for `find-generic-password` results; full
|
|
57
|
+
* macOS multi-account support would need either `dump-keychain` parsing
|
|
58
|
+
* or a Swift/native helper. Filed as a follow-up; the common case (one
|
|
59
|
+
* CC account in keychain) still works.
|
|
60
|
+
*
|
|
61
|
+
* Returns an empty array on any failure (keychain unavailable, no entries
|
|
62
|
+
* matching, parse errors). Callers are expected to handle empty as
|
|
63
|
+
* "nothing to import."
|
|
64
|
+
*/
|
|
65
|
+
export declare function enumerateKeychainCredentials(): Promise<KeychainEntry[]>;
|
|
25
66
|
export declare function loadCredentials(): Promise<CredentialsFile | null>;
|
|
26
67
|
/**
|
|
27
68
|
* Pick the freshest of a set of `CredentialsFile` candidates by
|
|
@@ -103,11 +144,19 @@ export declare function refreshTokens(): Promise<OAuthTokens>;
|
|
|
103
144
|
export declare function getAccessToken(): Promise<string>;
|
|
104
145
|
/**
|
|
105
146
|
* Get token status info.
|
|
147
|
+
*
|
|
148
|
+
* `status` returns 'broken' when refresh has failed REFRESH_BROKEN_THRESHOLD
|
|
149
|
+
* times in a row — this matters because the access token can still be ticking
|
|
150
|
+
* down (so naive "expiresIn" looks fine) while every actual upstream call
|
|
151
|
+
* returns 401. Operators relying on /health for a docker healthcheck or for
|
|
152
|
+
* `depends_on: service_healthy` need to see this state.
|
|
106
153
|
*/
|
|
107
154
|
export declare function getStatus(): Promise<{
|
|
108
155
|
authenticated: boolean;
|
|
109
|
-
status: 'healthy' | 'expiring' | 'expired' | 'none';
|
|
156
|
+
status: 'healthy' | 'expiring' | 'expired' | 'broken' | 'none';
|
|
110
157
|
expiresAt?: number;
|
|
111
158
|
expiresIn?: string;
|
|
112
159
|
canRefresh?: boolean;
|
|
160
|
+
refreshFailures?: number;
|
|
161
|
+
lastRefreshError?: string;
|
|
113
162
|
}>;
|
package/dist/oauth.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomBytes, createHash } from 'node:crypto';
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
-
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
9
|
+
import { readFile, writeFile, mkdir, rename, unlink } from 'node:fs/promises';
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { homedir, platform } from 'node:os';
|
|
@@ -33,6 +33,12 @@ const REFRESH_BUFFER_MS = 30 * 60 * 1000;
|
|
|
33
33
|
// After a failed refresh, don't retry for 60s to avoid spam
|
|
34
34
|
let lastRefreshFailure = 0;
|
|
35
35
|
const REFRESH_COOLDOWN_MS = 60 * 1000;
|
|
36
|
+
// Track consecutive refresh failures so /health can surface a dead refresh
|
|
37
|
+
// token instead of cheerfully reporting `oauth: "expiring"` while every
|
|
38
|
+
// upstream call returns 401.
|
|
39
|
+
let consecutiveRefreshFailures = 0;
|
|
40
|
+
let lastRefreshError;
|
|
41
|
+
const REFRESH_BROKEN_THRESHOLD = 3;
|
|
36
42
|
// In-memory credential cache — avoids disk reads on every request
|
|
37
43
|
let credentialsCache = null;
|
|
38
44
|
let credentialsCacheTime = 0;
|
|
@@ -65,6 +71,34 @@ function getDarioCredentialsPath() {
|
|
|
65
71
|
function getClaudeCodeCredentialsPath() {
|
|
66
72
|
return join(homedir(), '.claude', '.credentials.json');
|
|
67
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Verify the credentials directory is writable BEFORE we open the OAuth URL.
|
|
76
|
+
*
|
|
77
|
+
* Why: an unwritable ~/.dario (e.g. EACCES from a `--user 0` docker op that
|
|
78
|
+
* left the volume owned by root) is a silent killer — the OAuth round-trip
|
|
79
|
+
* succeeds, the user pastes the code, and saveCredentials() crashes with
|
|
80
|
+
* EACCES on the .tmp file. The auth code is now consumed and unrecoverable;
|
|
81
|
+
* the user has to start over and re-paste a fresh code, only to hit the same
|
|
82
|
+
* EACCES. Probing first surfaces the permission problem cleanly while the
|
|
83
|
+
* user still holds an un-burned auth code.
|
|
84
|
+
*/
|
|
85
|
+
async function probeWritability() {
|
|
86
|
+
const dir = dirname(getDarioCredentialsPath());
|
|
87
|
+
await mkdir(dir, { recursive: true });
|
|
88
|
+
const probe = join(dir, `.write-probe.${process.pid}`);
|
|
89
|
+
try {
|
|
90
|
+
await writeFile(probe, '', { mode: 0o600 });
|
|
91
|
+
await unlink(probe);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const code = err?.code;
|
|
95
|
+
throw new Error(`Credentials directory is not writable: ${dir} (${code || 'unknown'}). ` +
|
|
96
|
+
`Fix permissions before running 'dario login' so the OAuth code isn't ` +
|
|
97
|
+
`consumed by a flow that can't persist the result. ` +
|
|
98
|
+
`On Docker volumes left root-owned by a '--user 0' op, run: ` +
|
|
99
|
+
`chown -R dario:dario ${dir}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
68
102
|
/**
|
|
69
103
|
* Read Claude Code credentials from the OS keychain.
|
|
70
104
|
*
|
|
@@ -113,6 +147,146 @@ if ([CM]::CredEnumerate('Claude Code-credentials*', 0, [ref]$count, [ref]$ptr))
|
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
`;
|
|
150
|
+
// Enumeration variant of WIN_CRED_SCRIPT — emits one line per credential
|
|
151
|
+
// formatted as `<TargetName>\t<JSON>` so the importer can label entries
|
|
152
|
+
// for the operator to disambiguate between accounts.
|
|
153
|
+
const WIN_CRED_ENUMERATE_SCRIPT = `
|
|
154
|
+
$ErrorActionPreference = 'Stop'
|
|
155
|
+
$sig = @"
|
|
156
|
+
using System;
|
|
157
|
+
using System.Runtime.InteropServices;
|
|
158
|
+
public class CM2 {
|
|
159
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
160
|
+
public struct CRED {
|
|
161
|
+
public uint Flags; public uint Type; public string TargetName;
|
|
162
|
+
public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LW;
|
|
163
|
+
public uint BlobSize; public IntPtr Blob;
|
|
164
|
+
public uint Persist; public uint AC; public IntPtr Attrs;
|
|
165
|
+
public string Alias; public string UN;
|
|
166
|
+
}
|
|
167
|
+
[DllImport("advapi32.dll", EntryPoint="CredEnumerateW", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
168
|
+
public static extern bool CredEnumerate(string filter, uint flag, out uint count, out IntPtr pCredentials);
|
|
169
|
+
[DllImport("advapi32.dll", EntryPoint="CredFree")]
|
|
170
|
+
public static extern void CredFree(IntPtr cred);
|
|
171
|
+
}
|
|
172
|
+
"@
|
|
173
|
+
Add-Type -TypeDefinition $sig
|
|
174
|
+
$count = 0
|
|
175
|
+
$ptr = [IntPtr]::Zero
|
|
176
|
+
if ([CM2]::CredEnumerate('Claude Code-credentials*', 0, [ref]$count, [ref]$ptr)) {
|
|
177
|
+
try {
|
|
178
|
+
for ($i = 0; $i -lt $count; $i++) {
|
|
179
|
+
$credPtr = [System.Runtime.InteropServices.Marshal]::ReadIntPtr($ptr, $i * [IntPtr]::Size)
|
|
180
|
+
$cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($credPtr, [type][CM2+CRED])
|
|
181
|
+
if ($cred.BlobSize -gt 0) {
|
|
182
|
+
$bytes = New-Object byte[] $cred.BlobSize
|
|
183
|
+
[System.Runtime.InteropServices.Marshal]::Copy($cred.Blob, $bytes, 0, $cred.BlobSize)
|
|
184
|
+
$blob = [System.Text.Encoding]::Unicode.GetString($bytes)
|
|
185
|
+
Write-Output ($cred.TargetName + "\`t" + $blob)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
[CM2]::CredFree($ptr)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
/**
|
|
194
|
+
* Enumerate every Claude Code keychain entry on this host. Pool-mode
|
|
195
|
+
* counterpart to `loadKeychainCredentials` (which returns the first hit
|
|
196
|
+
* for the single-account login flow). Used by `dario accounts add
|
|
197
|
+
* --from-keychain` to import without rerunning OAuth.
|
|
198
|
+
*
|
|
199
|
+
* Per-platform coverage:
|
|
200
|
+
* - **Linux**: `secret-tool search --all service "Claude Code-credentials"`
|
|
201
|
+
* enumerates every matching attribute set. Account name comes from the
|
|
202
|
+
* `account` attribute when set, otherwise the secret hash truncated.
|
|
203
|
+
* - **Windows**: PowerShell + CredEnumerate already iterates every
|
|
204
|
+
* matching credential (existing pattern just wasn't exposing the
|
|
205
|
+
* TargetName). New script variant emits target + JSON blob per line.
|
|
206
|
+
* - **macOS**: returns at most one entry. The `security` CLI doesn't
|
|
207
|
+
* expose a clean enumeration for `find-generic-password` results; full
|
|
208
|
+
* macOS multi-account support would need either `dump-keychain` parsing
|
|
209
|
+
* or a Swift/native helper. Filed as a follow-up; the common case (one
|
|
210
|
+
* CC account in keychain) still works.
|
|
211
|
+
*
|
|
212
|
+
* Returns an empty array on any failure (keychain unavailable, no entries
|
|
213
|
+
* matching, parse errors). Callers are expected to handle empty as
|
|
214
|
+
* "nothing to import."
|
|
215
|
+
*/
|
|
216
|
+
export async function enumerateKeychainCredentials() {
|
|
217
|
+
const out = [];
|
|
218
|
+
try {
|
|
219
|
+
if (platform() === 'darwin') {
|
|
220
|
+
// Single-entry path; multi-entry on macOS is a known limitation.
|
|
221
|
+
const single = await loadKeychainCredentials();
|
|
222
|
+
if (single)
|
|
223
|
+
out.push({ target: 'Claude Code-credentials', credentials: single });
|
|
224
|
+
}
|
|
225
|
+
else if (platform() === 'linux') {
|
|
226
|
+
const raw = await new Promise((resolve, reject) => {
|
|
227
|
+
execFile('secret-tool', ['search', '--all', 'service', 'Claude Code-credentials'], { timeout: 5000 }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
228
|
+
});
|
|
229
|
+
// secret-tool emits blocks separated by blank lines, with lines like:
|
|
230
|
+
// [/secret/Claude Code-credentials/0]
|
|
231
|
+
// label = ...
|
|
232
|
+
// secret = <json blob>
|
|
233
|
+
// attribute.account = <account name>
|
|
234
|
+
// attribute.service = Claude Code-credentials
|
|
235
|
+
let currentSecret;
|
|
236
|
+
let currentAccount;
|
|
237
|
+
const flush = () => {
|
|
238
|
+
if (!currentSecret)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(currentSecret);
|
|
242
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
243
|
+
out.push({
|
|
244
|
+
target: currentAccount || 'Claude Code-credentials',
|
|
245
|
+
credentials: parsed,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch { /* not a CC creds blob — skip */ }
|
|
250
|
+
currentSecret = undefined;
|
|
251
|
+
currentAccount = undefined;
|
|
252
|
+
};
|
|
253
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
254
|
+
if (line.startsWith('[/')) {
|
|
255
|
+
flush();
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (line.startsWith('secret = '))
|
|
259
|
+
currentSecret = line.slice('secret = '.length);
|
|
260
|
+
else if (line.startsWith('attribute.account = '))
|
|
261
|
+
currentAccount = line.slice('attribute.account = '.length);
|
|
262
|
+
}
|
|
263
|
+
flush();
|
|
264
|
+
}
|
|
265
|
+
else if (platform() === 'win32') {
|
|
266
|
+
const raw = await new Promise((resolve, reject) => {
|
|
267
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WIN_CRED_ENUMERATE_SCRIPT], { timeout: 5000, windowsHide: true }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
268
|
+
});
|
|
269
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
270
|
+
const tab = line.indexOf('\t');
|
|
271
|
+
if (tab < 0)
|
|
272
|
+
continue;
|
|
273
|
+
const target = line.slice(0, tab).trim();
|
|
274
|
+
const blob = line.slice(tab + 1).trim();
|
|
275
|
+
if (!target || !blob)
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(blob);
|
|
279
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
280
|
+
out.push({ target, credentials: parsed });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { /* skip non-credential entries */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { /* keychain unavailable / empty */ }
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
116
290
|
async function loadKeychainCredentials() {
|
|
117
291
|
try {
|
|
118
292
|
if (platform() === 'darwin') {
|
|
@@ -242,6 +416,8 @@ async function saveCredentials(creds) {
|
|
|
242
416
|
* Opens browser, captures the authorization code automatically.
|
|
243
417
|
*/
|
|
244
418
|
export async function startAutoOAuthFlow() {
|
|
419
|
+
// Fail fast on unwritable credentials dir BEFORE the auth code is issued.
|
|
420
|
+
await probeWritability();
|
|
245
421
|
const { createServer } = await import('node:http');
|
|
246
422
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
247
423
|
// 32 random bytes → 43-char base64url state. See dario#71 — Anthropic's
|
|
@@ -438,6 +614,8 @@ export function detectHeadlessEnvironment() {
|
|
|
438
614
|
* (it's CSRF protection for a redirect we don't have here).
|
|
439
615
|
*/
|
|
440
616
|
export async function startManualOAuthFlow() {
|
|
617
|
+
// Fail fast on unwritable credentials dir BEFORE the auth code is issued.
|
|
618
|
+
await probeWritability();
|
|
441
619
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
442
620
|
// 32 bytes — same reason as startAutoOAuthFlow. See dario#71.
|
|
443
621
|
const state = base64url(randomBytes(32));
|
|
@@ -555,6 +733,8 @@ async function doRefreshTokens() {
|
|
|
555
733
|
scopes: oauth.scopes,
|
|
556
734
|
};
|
|
557
735
|
await saveCredentials({ claudeAiOauth: tokens });
|
|
736
|
+
consecutiveRefreshFailures = 0;
|
|
737
|
+
lastRefreshError = undefined;
|
|
558
738
|
return tokens;
|
|
559
739
|
}
|
|
560
740
|
throw new Error('Token refresh failed after 3 attempts');
|
|
@@ -584,13 +764,26 @@ export async function getAccessToken() {
|
|
|
584
764
|
}
|
|
585
765
|
catch (err) {
|
|
586
766
|
lastRefreshFailure = Date.now();
|
|
587
|
-
|
|
767
|
+
consecutiveRefreshFailures++;
|
|
768
|
+
// Redact tokens/JWTs/Bearer values and truncate before storing — this
|
|
769
|
+
// string surfaces on /status and /health (CodeQL js/stack-trace-exposure
|
|
770
|
+
// dario#17). The raw err.message can include URLs, partial response
|
|
771
|
+
// bodies, and stack-derived paths from fetch/JSON-parse errors.
|
|
772
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
773
|
+
lastRefreshError = redactSecrets(raw.slice(0, 200));
|
|
774
|
+
console.error(`[dario] Refresh failed (${consecutiveRefreshFailures} consecutive): ${lastRefreshError}. Will retry in 60s. Run \`dario login\` if this persists.`);
|
|
588
775
|
// Return current token — it might still work for a few more minutes
|
|
589
776
|
return oauth.accessToken;
|
|
590
777
|
}
|
|
591
778
|
}
|
|
592
779
|
/**
|
|
593
780
|
* Get token status info.
|
|
781
|
+
*
|
|
782
|
+
* `status` returns 'broken' when refresh has failed REFRESH_BROKEN_THRESHOLD
|
|
783
|
+
* times in a row — this matters because the access token can still be ticking
|
|
784
|
+
* down (so naive "expiresIn" looks fine) while every actual upstream call
|
|
785
|
+
* returns 401. Operators relying on /health for a docker healthcheck or for
|
|
786
|
+
* `depends_on: service_healthy` need to see this state.
|
|
594
787
|
*/
|
|
595
788
|
export async function getStatus() {
|
|
596
789
|
const creds = await loadCredentials();
|
|
@@ -599,19 +792,29 @@ export async function getStatus() {
|
|
|
599
792
|
}
|
|
600
793
|
const { expiresAt } = creds.claudeAiOauth;
|
|
601
794
|
const now = Date.now();
|
|
795
|
+
const broken = consecutiveRefreshFailures >= REFRESH_BROKEN_THRESHOLD;
|
|
602
796
|
if (expiresAt < now) {
|
|
603
|
-
// Expired but has refresh token — can be refreshed
|
|
604
|
-
const canRefresh = !!creds.claudeAiOauth.refreshToken;
|
|
605
|
-
return {
|
|
797
|
+
// Expired but has refresh token — can be refreshed (unless refresh itself is dead)
|
|
798
|
+
const canRefresh = !!creds.claudeAiOauth.refreshToken && !broken;
|
|
799
|
+
return {
|
|
800
|
+
authenticated: false,
|
|
801
|
+
status: broken ? 'broken' : 'expired',
|
|
802
|
+
expiresAt,
|
|
803
|
+
canRefresh,
|
|
804
|
+
refreshFailures: consecutiveRefreshFailures,
|
|
805
|
+
lastRefreshError,
|
|
806
|
+
};
|
|
606
807
|
}
|
|
607
808
|
const ms = expiresAt - now;
|
|
608
809
|
const hours = Math.floor(ms / 3600000);
|
|
609
810
|
const mins = Math.floor((ms % 3600000) / 60000);
|
|
610
811
|
const expiresIn = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
611
812
|
return {
|
|
612
|
-
authenticated:
|
|
613
|
-
status: ms < REFRESH_BUFFER_MS ? 'expiring' : 'healthy',
|
|
813
|
+
authenticated: !broken,
|
|
814
|
+
status: broken ? 'broken' : (ms < REFRESH_BUFFER_MS ? 'expiring' : 'healthy'),
|
|
614
815
|
expiresAt,
|
|
615
816
|
expiresIn,
|
|
817
|
+
refreshFailures: consecutiveRefreshFailures || undefined,
|
|
818
|
+
lastRefreshError,
|
|
616
819
|
};
|
|
617
820
|
}
|
package/dist/proxy.js
CHANGED
|
@@ -792,14 +792,25 @@ export async function startProxy(opts = {}) {
|
|
|
792
792
|
// Strip query parameters for endpoint matching
|
|
793
793
|
const urlPath = req.url?.split('?')[0] ?? '';
|
|
794
794
|
// Health check
|
|
795
|
+
//
|
|
796
|
+
// Returns HTTP 503 when OAuth is in a state that will cause every upstream
|
|
797
|
+
// call to fail: refresh has failed N consecutive times ('broken'), or the
|
|
798
|
+
// access token is expired with no usable refresh path. Docker healthchecks
|
|
799
|
+
// and dependent services (`depends_on: service_healthy`) need this to
|
|
800
|
+
// react instead of cheerfully passing while every /v1/messages 401s.
|
|
795
801
|
if (urlPath === '/health' || urlPath === '/') {
|
|
796
802
|
const s = await getStatus();
|
|
797
|
-
|
|
803
|
+
const dead = s.status === 'broken' || s.status === 'none' ||
|
|
804
|
+
(s.status === 'expired' && s.canRefresh === false);
|
|
805
|
+
const httpStatus = dead ? 503 : 200;
|
|
806
|
+
res.writeHead(httpStatus, JSON_HEADERS);
|
|
798
807
|
res.end(JSON.stringify({
|
|
799
|
-
status: 'ok',
|
|
808
|
+
status: dead ? 'degraded' : 'ok',
|
|
800
809
|
oauth: s.status,
|
|
801
810
|
expiresIn: s.expiresIn,
|
|
802
811
|
requests: requestCount,
|
|
812
|
+
...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
|
|
813
|
+
...(s.lastRefreshError ? { lastRefreshError: s.lastRefreshError } : {}),
|
|
803
814
|
}));
|
|
804
815
|
return;
|
|
805
816
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.37.
|
|
3
|
+
"version": "3.37.12",
|
|
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": {
|