@askalf/dario 3.31.5 → 3.31.7

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Live probe of Anthropic's /oauth/authorize endpoint.
3
+ *
4
+ * Lifted from scripts/_authorize-probe-classifier.mjs and
5
+ * scripts/check-cc-authorize-probe.mjs so `dario doctor --probe` can
6
+ * reuse the same logic without re-importing from scripts. The .mjs
7
+ * files re-export from this module for the CI drift watcher.
8
+ *
9
+ * Motivating pattern: Anthropic's policy engine flips scope
10
+ * acceptability without changing what CC ships in its binary, so the
11
+ * binary-scan drift watcher can't see the flip. The live probe fills
12
+ * that gap — but from GitHub Actions IPs it hits Cloudflare's bot
13
+ * challenge and comes back "inconclusive" most of the time. Running
14
+ * the same probe from a user's own machine (via `dario doctor
15
+ * --probe`) is the workaround: CF doesn't challenge residential IPs
16
+ * the same way. See dario #42 / #71 for the incidents this prevents.
17
+ */
18
+ /**
19
+ * The specific string Anthropic's authorize endpoint returns for a
20
+ * scope set its policy engine rejects. Observed across multiple
21
+ * incidents (dario #22, #42, #71) — stable across the policy flips.
22
+ */
23
+ export declare const REJECT_MARKER = "Invalid request format";
24
+ /**
25
+ * The minimal response shape the classifier needs. `fetch` results can
26
+ * be mapped to this in one call (see `probeAuthorize` below).
27
+ */
28
+ export interface AuthorizeResponse {
29
+ status: number;
30
+ location: string | null;
31
+ body: string;
32
+ error: string | null;
33
+ }
34
+ export type ProbeVerdict = 'accepted' | 'rejected' | 'inconclusive';
35
+ export interface ClassifiedVerdict {
36
+ verdict: ProbeVerdict;
37
+ reason: string;
38
+ }
39
+ /**
40
+ * Classify a single authorize-endpoint response.
41
+ *
42
+ * accepted — 3xx redirect to login/consent, OR 2xx without the
43
+ * reject marker. The scope set passed validation.
44
+ * rejected — body contains the specific "Invalid request format"
45
+ * marker. The scope set was refused.
46
+ * inconclusive — fetch error, Cloudflare challenge, or any other
47
+ * response shape we can't categorize. Not drift; try
48
+ * again.
49
+ */
50
+ export declare function classifyAuthorizeResponse({ status, location, body, error }: AuthorizeResponse): ClassifiedVerdict;
51
+ export interface DriftItem {
52
+ probe: 'A' | 'B';
53
+ severity: 'high' | 'medium' | 'info';
54
+ message: string;
55
+ }
56
+ export interface CombinedVerdict {
57
+ outcome: 'clean' | 'drift' | 'inconclusive';
58
+ drift: boolean;
59
+ items: DriftItem[];
60
+ }
61
+ /**
62
+ * Combine the verdicts for probe A (pinned 6-scope) and probe B
63
+ * (5-scope, org:create_api_key removed) into a single watcher result.
64
+ * See scripts/check-cc-authorize-probe.mjs for the full rationale.
65
+ */
66
+ export declare function combineVerdicts(a: ClassifiedVerdict, b: ClassifiedVerdict): CombinedVerdict;
67
+ export interface ProbeConfig {
68
+ clientId: string;
69
+ authorizeUrl: string;
70
+ scopes: string;
71
+ }
72
+ export interface ProbeOptions {
73
+ timeoutMs?: number;
74
+ /** Test hook. Default: `globalThis.fetch`. */
75
+ fetchImpl?: typeof fetch;
76
+ }
77
+ export interface ProbeResult extends ClassifiedVerdict {
78
+ /** The exact URL sent — useful for the user to paste into a browser if the verdict is rejected. */
79
+ probedUrl: string;
80
+ /** Scope count from `scopes`, for quick human read-out. */
81
+ scopeCount: number;
82
+ }
83
+ export declare function buildProbeAuthorizeUrl(cfg: ProbeConfig): string;
84
+ /**
85
+ * Probe the authorize endpoint with the given config. Follows exactly
86
+ * one redirect hop if the Location points at a trusted Anthropic host
87
+ * (claude.ai / claude.com / *.anthropic.com) — the legacy
88
+ * `claude.com/cai/oauth/authorize` edge 307s to claude.ai and the
89
+ * destination is where the validation happens. Stopping at the edge
90
+ * would silently treat every scope set as accepted.
91
+ */
92
+ export declare function runAuthorizeProbe(cfg: ProbeConfig, opts?: ProbeOptions): Promise<ProbeResult>;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Live probe of Anthropic's /oauth/authorize endpoint.
3
+ *
4
+ * Lifted from scripts/_authorize-probe-classifier.mjs and
5
+ * scripts/check-cc-authorize-probe.mjs so `dario doctor --probe` can
6
+ * reuse the same logic without re-importing from scripts. The .mjs
7
+ * files re-export from this module for the CI drift watcher.
8
+ *
9
+ * Motivating pattern: Anthropic's policy engine flips scope
10
+ * acceptability without changing what CC ships in its binary, so the
11
+ * binary-scan drift watcher can't see the flip. The live probe fills
12
+ * that gap — but from GitHub Actions IPs it hits Cloudflare's bot
13
+ * challenge and comes back "inconclusive" most of the time. Running
14
+ * the same probe from a user's own machine (via `dario doctor
15
+ * --probe`) is the workaround: CF doesn't challenge residential IPs
16
+ * the same way. See dario #42 / #71 for the incidents this prevents.
17
+ */
18
+ import { createHash, randomBytes } from 'node:crypto';
19
+ /**
20
+ * The specific string Anthropic's authorize endpoint returns for a
21
+ * scope set its policy engine rejects. Observed across multiple
22
+ * incidents (dario #22, #42, #71) — stable across the policy flips.
23
+ */
24
+ export const REJECT_MARKER = 'Invalid request format';
25
+ /** Not a real callback — the probe only needs the initial response. */
26
+ const PROBE_REDIRECT_URI = 'http://localhost:12345/callback';
27
+ const PROBE_TIMEOUT_MS = 15_000;
28
+ /** Cloudflare fronts claude.ai and challenges unrecognized clients. */
29
+ function isCloudflareChallenge({ status, body }) {
30
+ const bodyText = typeof body === 'string' ? body : '';
31
+ if (bodyText.includes('Just a moment...') || bodyText.includes('/cdn-cgi/challenge-platform/')) {
32
+ return true;
33
+ }
34
+ if (status === 403 && bodyText.includes('cdn-cgi')) {
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ /**
40
+ * Classify a single authorize-endpoint response.
41
+ *
42
+ * accepted — 3xx redirect to login/consent, OR 2xx without the
43
+ * reject marker. The scope set passed validation.
44
+ * rejected — body contains the specific "Invalid request format"
45
+ * marker. The scope set was refused.
46
+ * inconclusive — fetch error, Cloudflare challenge, or any other
47
+ * response shape we can't categorize. Not drift; try
48
+ * again.
49
+ */
50
+ export function classifyAuthorizeResponse({ status, location, body, error }) {
51
+ if (error) {
52
+ return { verdict: 'inconclusive', reason: `fetch error: ${error}` };
53
+ }
54
+ if (isCloudflareChallenge({ status, body })) {
55
+ return {
56
+ verdict: 'inconclusive',
57
+ reason: `blocked by Cloudflare bot challenge (status=${status}). ` +
58
+ `The live probe is unreliable from CI IPs — rely on the scope-literal ` +
59
+ `scan in check-cc-drift.mjs, or run this probe from a trusted network.`,
60
+ };
61
+ }
62
+ const bodyText = typeof body === 'string' ? body : '';
63
+ if (bodyText.includes(REJECT_MARKER)) {
64
+ return { verdict: 'rejected', reason: `body contains "${REJECT_MARKER}"` };
65
+ }
66
+ if (status >= 300 && status < 400 && typeof location === 'string' && location.length > 0) {
67
+ return { verdict: 'accepted', reason: `${status} redirect to ${location}` };
68
+ }
69
+ if (status >= 200 && status < 300) {
70
+ return { verdict: 'accepted', reason: `${status} body rendered, no reject marker` };
71
+ }
72
+ return {
73
+ verdict: 'inconclusive',
74
+ reason: `unexpected response: status=${status}, location=${location ?? 'none'}, body_len=${bodyText.length}`,
75
+ };
76
+ }
77
+ /**
78
+ * Combine the verdicts for probe A (pinned 6-scope) and probe B
79
+ * (5-scope, org:create_api_key removed) into a single watcher result.
80
+ * See scripts/check-cc-authorize-probe.mjs for the full rationale.
81
+ */
82
+ export function combineVerdicts(a, b) {
83
+ if (a.verdict === 'inconclusive' || b.verdict === 'inconclusive') {
84
+ const items = [];
85
+ if (a.verdict === 'inconclusive')
86
+ items.push({ probe: 'A', severity: 'info', message: `probe A inconclusive: ${a.reason}` });
87
+ if (b.verdict === 'inconclusive')
88
+ items.push({ probe: 'B', severity: 'info', message: `probe B inconclusive: ${b.reason}` });
89
+ return { outcome: 'inconclusive', drift: false, items };
90
+ }
91
+ const items = [];
92
+ if (a.verdict !== 'accepted') {
93
+ items.push({
94
+ probe: 'A',
95
+ severity: 'high',
96
+ message: `Pinned FALLBACK.scopes (6-scope) no longer accepted by authorize endpoint (${a.reason}). ` +
97
+ `This is the dario #42 / #71 failure mode: users will hit "Invalid request format" on fresh login. ` +
98
+ `Investigate which scope the server now rejects and update FALLBACK.scopes in src/cc-oauth-detect.ts; ` +
99
+ `bump the cache suffix (CACHE_PATH) so existing users regenerate.`,
100
+ });
101
+ }
102
+ if (b.verdict === 'accepted') {
103
+ items.push({
104
+ probe: 'B',
105
+ severity: 'info',
106
+ message: `5-scope form (org:create_api_key removed) is ALSO accepted. Anthropic's authorize ` +
107
+ `endpoint appears permissive for this client_id — both the 6-scope form our users send ` +
108
+ `and the 5-scope form are accepted. Nothing to fix; recorded so we notice when it flips.`,
109
+ });
110
+ }
111
+ const hasDrift = items.some((i) => i.severity === 'high');
112
+ return {
113
+ outcome: hasDrift ? 'drift' : 'clean',
114
+ drift: hasDrift,
115
+ items,
116
+ };
117
+ }
118
+ function base64url(buf) {
119
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
120
+ }
121
+ function pkceChallenge() {
122
+ const verifier = base64url(randomBytes(32));
123
+ return base64url(createHash('sha256').update(verifier).digest());
124
+ }
125
+ export function buildProbeAuthorizeUrl(cfg) {
126
+ const params = new URLSearchParams({
127
+ code: 'true',
128
+ client_id: cfg.clientId,
129
+ response_type: 'code',
130
+ redirect_uri: PROBE_REDIRECT_URI,
131
+ scope: cfg.scopes,
132
+ code_challenge: pkceChallenge(),
133
+ code_challenge_method: 'S256',
134
+ state: base64url(randomBytes(16)),
135
+ });
136
+ return `${cfg.authorizeUrl}?${params.toString()}`;
137
+ }
138
+ const PROBE_HEADERS = {
139
+ 'User-Agent': 'dario-cc-authorize-probe/1 (+https://github.com/askalf/dario)',
140
+ Accept: 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
141
+ };
142
+ async function fetchOnce(url, opts) {
143
+ const fetchImpl = opts.fetchImpl ?? fetch;
144
+ const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
145
+ try {
146
+ const res = await fetchImpl(url, {
147
+ method: 'GET',
148
+ redirect: 'manual',
149
+ signal: AbortSignal.timeout(timeoutMs),
150
+ headers: PROBE_HEADERS,
151
+ });
152
+ const location = res.headers.get('location');
153
+ let body = '';
154
+ try {
155
+ body = await res.text();
156
+ }
157
+ catch (err) {
158
+ return { status: res.status, location, body: '', error: `read body failed: ${err?.message ?? String(err)}` };
159
+ }
160
+ return { status: res.status, location, body, error: null };
161
+ }
162
+ catch (err) {
163
+ return { status: 0, location: null, body: '', error: err?.message ?? String(err) };
164
+ }
165
+ }
166
+ /**
167
+ * Probe the authorize endpoint with the given config. Follows exactly
168
+ * one redirect hop if the Location points at a trusted Anthropic host
169
+ * (claude.ai / claude.com / *.anthropic.com) — the legacy
170
+ * `claude.com/cai/oauth/authorize` edge 307s to claude.ai and the
171
+ * destination is where the validation happens. Stopping at the edge
172
+ * would silently treat every scope set as accepted.
173
+ */
174
+ export async function runAuthorizeProbe(cfg, opts = {}) {
175
+ const url = buildProbeAuthorizeUrl(cfg);
176
+ const first = await fetchOnce(url, opts);
177
+ const trustedRedirect = first.status >= 300 && first.status < 400 &&
178
+ typeof first.location === 'string' &&
179
+ /^https:\/\/(claude\.ai|claude\.com|[\w-]+\.anthropic\.com)\//.test(first.location);
180
+ const response = trustedRedirect && first.location !== null
181
+ ? await fetchOnce(first.location, opts)
182
+ : first;
183
+ const classified = classifyAuthorizeResponse(response);
184
+ const scopeCount = cfg.scopes.split(/\s+/).filter(Boolean).length;
185
+ return { ...classified, probedUrl: url, scopeCount };
186
+ }
package/dist/cli.js CHANGED
@@ -643,6 +643,11 @@ async function help() {
643
643
  CLI. (v3.27)
644
644
  dario doctor Print a health report: dario / Node / CC /
645
645
  template / drift / OAuth / pool / backends
646
+ dario doctor --probe Also hit Anthropic's authorize endpoint with
647
+ dario's effective OAuth config and surface
648
+ the server's verdict — the single reliable
649
+ signal for scope-policy drift (dario#42/#71
650
+ class). One GET to claude.ai; no PII.
646
651
 
647
652
  Proxy options:
648
653
  --model=MODEL Force a model for all requests
@@ -943,16 +948,20 @@ async function mcp() {
943
948
  }
944
949
  async function doctor() {
945
950
  const { runChecks, formatChecks, exitCodeFor } = await import('./doctor.js');
951
+ const probe = args.includes('--probe');
946
952
  console.log('');
947
953
  console.log(' dario — Doctor');
948
954
  console.log(' ─────────────');
949
955
  console.log('');
950
- const checks = await runChecks();
956
+ const checks = await runChecks({ probe });
951
957
  console.log(formatChecks(checks));
952
958
  console.log('');
953
959
  const code = exitCodeFor(checks);
954
960
  if (code !== 0) {
955
961
  console.log(' One or more checks failed. Address the [FAIL] rows and re-run `dario doctor`.');
962
+ if (!probe) {
963
+ console.log(' For a live check against Anthropic\'s authorize endpoint, re-run with `--probe`.');
964
+ }
956
965
  console.log('');
957
966
  }
958
967
  process.exit(code);
package/dist/doctor.d.ts CHANGED
@@ -31,6 +31,18 @@ export declare function formatChecks(checks: Check[]): string;
31
31
  * a user's machine just because they're on an untested CC version.
32
32
  */
33
33
  export declare function exitCodeFor(checks: Check[]): number;
34
+ export interface RunChecksOptions {
35
+ /**
36
+ * Opt-in: hit Anthropic's authorize endpoint with the scope set dario
37
+ * would use on `accounts add`, and surface the server's verdict as a
38
+ * check row. Default off — `dario doctor` without `--probe` is a
39
+ * read-only local scan, no outbound traffic beyond what the other
40
+ * checks already make (OAuth token refresh, CC binary version probe,
41
+ * npm drift check). Enable with `dario doctor --probe`; costs one
42
+ * GET to `claude.ai` and runs in parallel with the other checks.
43
+ */
44
+ probe?: boolean;
45
+ }
34
46
  /**
35
47
  * Run every available health check. Never throws — each check is
36
48
  * individually try/caught so a broken subsystem (e.g. unreadable accounts
@@ -40,4 +52,4 @@ export declare function exitCodeFor(checks: Check[]): number;
40
52
  * version, platform) so a reader scanning the output top-down sees
41
53
  * the environment before the subsystems.
42
54
  */
43
- export declare function runChecks(): Promise<Check[]>;
55
+ export declare function runChecks(opts?: RunChecksOptions): Promise<Check[]>;
package/dist/doctor.js CHANGED
@@ -16,6 +16,8 @@ import { fileURLToPath } from 'node:url';
16
16
  import { homedir, platform, arch, release } from 'node:os';
17
17
  import { CC_TEMPLATE, } from './cc-template.js';
18
18
  import { describeTemplate, detectDrift, checkCCCompat, findInstalledCC, SUPPORTED_CC_RANGE, CURRENT_SCHEMA_VERSION, } from './live-fingerprint.js';
19
+ import { detectCCOAuthConfig } from './cc-oauth-detect.js';
20
+ import { runAuthorizeProbe } from './cc-authorize-probe.js';
19
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
22
  /**
21
23
  * Pretty-print a list of Check results as aligned ASCII. No color codes —
@@ -50,7 +52,7 @@ export function exitCodeFor(checks) {
50
52
  * version, platform) so a reader scanning the output top-down sees
51
53
  * the environment before the subsystems.
52
54
  */
53
- export async function runChecks() {
55
+ export async function runChecks(opts = {}) {
54
56
  const checks = [];
55
57
  // ---- dario version
56
58
  try {
@@ -159,6 +161,50 @@ export async function runChecks() {
159
161
  catch (err) {
160
162
  checks.push({ status: 'warn', label: 'OAuth', detail: `check failed: ${err.message}` });
161
163
  }
164
+ // ---- Authorize-URL probe (opt-in, --probe).
165
+ // One GET to the authorize endpoint with dario's effective OAuth config.
166
+ // This is the single reliable signal for the class of bug that broke
167
+ // #42 / #71 — Anthropic flipping server-side scope policy without
168
+ // changing the CC binary. The nightly probe in check-cc-authorize-
169
+ // probe.mjs hits Cloudflare challenges from CI IPs; running from a
170
+ // user's machine bypasses that. No PII leaves: the probe uses a
171
+ // fresh PKCE challenge and a dummy redirect_uri, and only reads the
172
+ // status code / Location header / response body markers.
173
+ if (opts.probe) {
174
+ try {
175
+ const cfg = await detectCCOAuthConfig();
176
+ const result = await runAuthorizeProbe({
177
+ clientId: cfg.clientId,
178
+ authorizeUrl: cfg.authorizeUrl,
179
+ scopes: cfg.scopes,
180
+ });
181
+ const status = result.verdict === 'accepted'
182
+ ? 'ok'
183
+ : result.verdict === 'rejected'
184
+ ? 'fail'
185
+ : 'warn';
186
+ const label = 'Authorize probe';
187
+ const summary = `${result.scopeCount}-scope ${result.verdict} — ${result.reason}`;
188
+ checks.push({ status, label, detail: summary });
189
+ if (result.verdict !== 'accepted') {
190
+ // On rejection: the URL is the one `accounts add` would open —
191
+ // surface it so the user can paste and diff against `claude
192
+ // /login`'s URL. On inconclusive (often Cloudflare from our
193
+ // fetch-based probe — CF challenges non-browser clients
194
+ // regardless of IP): the same URL pasted into the user's
195
+ // browser bypasses CF since a real browser passes the
196
+ // challenge. Either way, the URL is the actionable artifact.
197
+ checks.push({ status: 'info', label: 'Probe URL', detail: result.probedUrl });
198
+ }
199
+ }
200
+ catch (err) {
201
+ checks.push({
202
+ status: 'warn',
203
+ label: 'Authorize probe',
204
+ detail: `check failed: ${err.message}`,
205
+ });
206
+ }
207
+ }
162
208
  // ---- Account pool
163
209
  try {
164
210
  const { listAccountAliases, loadAllAccounts } = await import('./accounts.js');
@@ -210,6 +210,7 @@ export declare function findInstalledCC(): {
210
210
  path: string | null;
211
211
  version: string | null;
212
212
  };
213
+ export declare function enumerateClaudeCandidates(): string[];
213
214
  /**
214
215
  * Given a captured /v1/messages request body, pull out the fields that
215
216
  * matter for template replay: agent identity, system prompt, tool list,
@@ -449,30 +449,77 @@ function findClaudeBinary() {
449
449
  // non-standard installs.
450
450
  if (process.env.DARIO_CLAUDE_BIN)
451
451
  return process.env.DARIO_CLAUDE_BIN;
452
- // Try the obvious name. On Windows spawn resolves `.cmd` shims
453
- // automatically when shell:true, but we don't want shell:true for
454
- // safety. The `where` / `which` probe handles Windows via PATHEXT.
455
- const candidates = process.platform === 'win32'
456
- ? ['claude.cmd', 'claude.exe', 'claude']
457
- : ['claude'];
458
- for (const name of candidates) {
459
- if (existsOnPath(name))
460
- return name;
452
+ const candidates = enumerateClaudeCandidates();
453
+ if (candidates.length === 0)
454
+ return null;
455
+ if (candidates.length === 1)
456
+ return candidates[0];
457
+ // Multiple installs on PATH — common on Windows where an npm-wrapper
458
+ // (~/AppData/Roaming/npm/claude.cmd) coexists with a native install
459
+ // (~/.local/bin/claude.exe). Version-probe each and pick the newest.
460
+ // Falls back to the first candidate if no probe succeeds (e.g. every
461
+ // spawn fails on a sandboxed runtime).
462
+ const probed = [];
463
+ for (const path of candidates) {
464
+ const version = probeOneVersion(path);
465
+ if (version)
466
+ probed.push({ path, version });
461
467
  }
462
- return null;
468
+ if (probed.length === 0)
469
+ return candidates[0];
470
+ probed.sort((a, b) => compareVersions(b.version, a.version));
471
+ return probed[0].path;
463
472
  }
464
- function existsOnPath(name) {
473
+ // Exported for unit tests.
474
+ export function enumerateClaudeCandidates() {
465
475
  const pathEnv = process.env.PATH ?? '';
466
476
  const sep = process.platform === 'win32' ? ';' : ':';
467
477
  const dirs = pathEnv.split(sep).filter(Boolean);
478
+ // `.exe` first on Windows: the native binary beats a `.cmd` wrapper
479
+ // when both live in the same dir. Across dirs we version-probe anyway
480
+ // so order here only matters when probes all fail.
481
+ const names = process.platform === 'win32'
482
+ ? ['claude.exe', 'claude.cmd', 'claude']
483
+ : ['claude'];
484
+ const found = [];
485
+ const seen = new Set();
468
486
  for (const d of dirs) {
469
- try {
470
- if (existsSync(join(d, name)))
471
- return true;
487
+ for (const name of names) {
488
+ const full = join(d, name);
489
+ if (seen.has(full))
490
+ continue;
491
+ try {
492
+ if (existsSync(full)) {
493
+ seen.add(full);
494
+ found.push(full);
495
+ }
496
+ }
497
+ catch { /* noop */ }
472
498
  }
473
- catch { /* noop */ }
474
499
  }
475
- return false;
500
+ return found;
501
+ }
502
+ // Version-probe one specific binary path. Same safety logic as
503
+ // probeInstalledCCVersionUncached below (reject shell metacharacters in
504
+ // override paths before spawning with shell:true on Windows).
505
+ function probeOneVersion(bin) {
506
+ try {
507
+ const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
508
+ if (useShell && /[&|><^"'%\r\n`$;(){}[\]]/.test(bin))
509
+ return null;
510
+ const out = execFileSync(bin, ['--version'], {
511
+ encoding: 'utf-8',
512
+ timeout: 2_000,
513
+ stdio: ['ignore', 'pipe', 'ignore'],
514
+ windowsHide: true,
515
+ shell: useShell,
516
+ });
517
+ const m = /(\d+\.\d+\.\d+(?:[.\-][\w.\-]+)?)/.exec(out);
518
+ return m ? m[1] : null;
519
+ }
520
+ catch {
521
+ return null;
522
+ }
476
523
  }
477
524
  /**
478
525
  * Given a captured /v1/messages request body, pull out the fields that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.31.5",
3
+ "version": "3.31.7",
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": {