@askalf/dario 3.31.7 → 3.31.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/dist/accounts.js +8 -1
- package/dist/cc-oauth-detect.d.ts +10 -0
- package/dist/cc-oauth-detect.js +44 -15
- package/dist/cli.js +147 -2
- package/dist/config-report.d.ts +43 -0
- package/dist/config-report.js +187 -0
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +235 -1
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -195,7 +195,14 @@ function openBrowser(url) {
|
|
|
195
195
|
export async function addAccountViaOAuth(alias) {
|
|
196
196
|
const cfg = await detectCCOAuthConfig();
|
|
197
197
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
198
|
-
|
|
198
|
+
// 32 random bytes → 43-char base64url state. Matches what CC v2.1.116+
|
|
199
|
+
// ships in `/login` URLs; Anthropic's `/oauth/authorize` endpoint started
|
|
200
|
+
// rejecting shorter states with "Invalid request format" on 2026-04-23
|
|
201
|
+
// (dario#71 repro: URL was byte-equivalent to CC's except state was
|
|
202
|
+
// 22 chars → reject, 43 chars → accept). RFC 6749 only requires
|
|
203
|
+
// "non-guessable," so shorter is technically legal — Anthropic's stricter
|
|
204
|
+
// than spec here. Keep in lockstep with CC's bytes-per-random.
|
|
205
|
+
const state = base64url(randomBytes(32));
|
|
199
206
|
return new Promise((resolve, reject) => {
|
|
200
207
|
let port = 0;
|
|
201
208
|
const server = createServer(async (req, res) => {
|
|
@@ -75,6 +75,16 @@ export declare const FALLBACK_FOR_DRIFT_CHECK: Readonly<DetectedOAuthConfig>;
|
|
|
75
75
|
* window after it.
|
|
76
76
|
*/
|
|
77
77
|
export declare function scanBinaryForOAuthConfig(buf: Buffer): Omit<DetectedOAuthConfig, 'source' | 'ccPath' | 'ccHash'> | null;
|
|
78
|
+
/**
|
|
79
|
+
* Given CC's binary and a list of scope literals we expect to find,
|
|
80
|
+
* return the subset that actually appear as quoted-string literals in
|
|
81
|
+
* the binary. Scoping the match to `"<scope>"` (with surrounding
|
|
82
|
+
* double quotes) avoids false matches on partial substrings. Pure
|
|
83
|
+
* over its inputs — safe to unit-test without a real CC binary.
|
|
84
|
+
*
|
|
85
|
+
* Exported for unit tests.
|
|
86
|
+
*/
|
|
87
|
+
export declare function filterScopesByBinaryPresence(buf: Buffer, expected: readonly string[]): string[];
|
|
78
88
|
/**
|
|
79
89
|
* Get the OAuth config for dario to use. Scans the installed CC binary
|
|
80
90
|
* on first call, caches to disk, and memoizes in-process for subsequent
|
package/dist/cc-oauth-detect.js
CHANGED
|
@@ -273,22 +273,51 @@ export function scanBinaryForOAuthConfig(buf) {
|
|
|
273
273
|
const tokenMatch = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(prodBlock);
|
|
274
274
|
if (tokenMatch && tokenMatch[1])
|
|
275
275
|
tokenUrl = tokenMatch[1];
|
|
276
|
-
// Scopes
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// contains only 4 of the 6 actual scopes. The real scope array is stored
|
|
281
|
-
// as a constant-reference array — `dY8 = [B9H, TI, "user:sessions:...", ...]`
|
|
282
|
-
// — where the first two elements are minified variable references, not
|
|
283
|
-
// literal strings, so no regex can reliably extract the full list. And the
|
|
284
|
-
// runtime-computed union `n36` only exists after `Array.from(new Set(...))`
|
|
285
|
-
// executes, which we can't evaluate from a static scan.
|
|
276
|
+
// Scopes can't be EXTRACTED from the binary — the real scope array is
|
|
277
|
+
// stored as a constant-reference array (`dY8 = [B9H, TI, "user:sessions:
|
|
278
|
+
// ...", ...]`) where the first elements are minified variable references,
|
|
279
|
+
// not literal strings, so no regex resolves the full list statically.
|
|
286
280
|
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
281
|
+
// But we CAN verify them: every scope CC uses does appear as a quoted
|
|
282
|
+
// literal string somewhere in the binary (either in the scope array
|
|
283
|
+
// itself when not minified to a variable ref, or in help-text /
|
|
284
|
+
// error-message references that name scopes by literal). Scan for each
|
|
285
|
+
// FALLBACK scope's quoted literal; if any go missing, drop those
|
|
286
|
+
// scopes from the returned config and log a warning.
|
|
287
|
+
//
|
|
288
|
+
// This catches one class of drift the hardcoded FALLBACK doesn't: a
|
|
289
|
+
// future CC release that REMOVES a scope from the active set — Anthropic
|
|
290
|
+
// deprecates `user:file_upload` in CC v2.2.0, say — would leave dario
|
|
291
|
+
// sending a stale scope that the server now rejects, same failure mode
|
|
292
|
+
// as #42 / #71. Binary verification catches it at startup without
|
|
293
|
+
// waiting for a user to hit the "Invalid request format" page.
|
|
294
|
+
//
|
|
295
|
+
// It does NOT catch the opposite direction (Anthropic starts accepting
|
|
296
|
+
// a new scope CC didn't previously use) — those still require a FALLBACK
|
|
297
|
+
// bump. The probe in scripts/check-cc-authorize-probe.mjs + `dario
|
|
298
|
+
// doctor --probe` covers that direction.
|
|
299
|
+
const expected = FALLBACK.scopes.split(/\s+/).filter(Boolean);
|
|
300
|
+
const verified = filterScopesByBinaryPresence(buf, expected);
|
|
301
|
+
const scopes = verified.length > 0 ? verified.join(' ') : FALLBACK.scopes;
|
|
302
|
+
return { clientId, authorizeUrl, tokenUrl, scopes };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Given CC's binary and a list of scope literals we expect to find,
|
|
306
|
+
* return the subset that actually appear as quoted-string literals in
|
|
307
|
+
* the binary. Scoping the match to `"<scope>"` (with surrounding
|
|
308
|
+
* double quotes) avoids false matches on partial substrings. Pure
|
|
309
|
+
* over its inputs — safe to unit-test without a real CC binary.
|
|
310
|
+
*
|
|
311
|
+
* Exported for unit tests.
|
|
312
|
+
*/
|
|
313
|
+
export function filterScopesByBinaryPresence(buf, expected) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const scope of expected) {
|
|
316
|
+
const needle = Buffer.from(`"${scope}"`);
|
|
317
|
+
if (buf.includes(needle))
|
|
318
|
+
out.push(scope);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
292
321
|
}
|
|
293
322
|
async function loadCache() {
|
|
294
323
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -648,6 +648,25 @@ async function help() {
|
|
|
648
648
|
the server's verdict — the single reliable
|
|
649
649
|
signal for scope-policy drift (dario#42/#71
|
|
650
650
|
class). One GET to claude.ai; no PII.
|
|
651
|
+
dario doctor --json Emit the check report as structured JSON
|
|
652
|
+
for machine consumption (claude-bridge
|
|
653
|
+
/status, CI scripts, etc.) instead of the
|
|
654
|
+
human-readable table.
|
|
655
|
+
dario doctor --auth-check
|
|
656
|
+
One-shot listener on an ephemeral loopback
|
|
657
|
+
port. Send ONE request from your client
|
|
658
|
+
(OpenClaw, Hermes, curl, etc.) and dario
|
|
659
|
+
classifies the auth headers it sees against
|
|
660
|
+
DARIO_API_KEY — with redacted previews and
|
|
661
|
+
a targeted diagnosis (dario#97 class). Use
|
|
662
|
+
--timeout-ms=N to adjust the 30s default.
|
|
663
|
+
dario config Print the effective configuration (port,
|
|
664
|
+
host, DARIO_API_KEY state, OAuth status,
|
|
665
|
+
pool, backends, paths) with credentials
|
|
666
|
+
redacted. Safe to paste into bug reports.
|
|
667
|
+
--json for structured output.
|
|
668
|
+
dario upgrade npm install -g @askalf/dario@latest with a
|
|
669
|
+
pre-flight current-vs-latest check.
|
|
651
670
|
|
|
652
671
|
Proxy options:
|
|
653
672
|
--model=MODEL Force a model for all requests
|
|
@@ -947,13 +966,57 @@ async function mcp() {
|
|
|
947
966
|
}
|
|
948
967
|
}
|
|
949
968
|
async function doctor() {
|
|
950
|
-
const { runChecks, formatChecks, exitCodeFor } = await import('./doctor.js');
|
|
969
|
+
const { runChecks, formatChecks, formatChecksJson, exitCodeFor, runAuthCheck } = await import('./doctor.js');
|
|
951
970
|
const probe = args.includes('--probe');
|
|
971
|
+
const asJson = args.includes('--json');
|
|
972
|
+
const authCheck = args.includes('--auth-check');
|
|
973
|
+
if (authCheck) {
|
|
974
|
+
console.log('');
|
|
975
|
+
console.log(' dario — Auth Check');
|
|
976
|
+
console.log(' ──────────────────');
|
|
977
|
+
console.log('');
|
|
978
|
+
const timeoutArg = args.find((a) => a.startsWith('--timeout-ms='));
|
|
979
|
+
const timeoutMs = timeoutArg ? Math.max(1000, parseInt(timeoutArg.split('=')[1], 10)) : 30_000;
|
|
980
|
+
const result = await runAuthCheck({
|
|
981
|
+
timeoutMs,
|
|
982
|
+
onListening: (port) => {
|
|
983
|
+
console.log(` Listening on http://127.0.0.1:${port}/`);
|
|
984
|
+
console.log(` Waiting up to ${Math.round(timeoutMs / 1000)}s for ONE request from your client.`);
|
|
985
|
+
console.log(' Any path and method are fine — dario only inspects the auth headers.');
|
|
986
|
+
console.log('');
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
console.log(` Verdict: ${result.verdict}`);
|
|
990
|
+
console.log(` Expected: ${result.expected === '<unset>' ? '(unset)' : 'Bearer <key> (DARIO_API_KEY matches)'}`);
|
|
991
|
+
if (result.received) {
|
|
992
|
+
const seen = [];
|
|
993
|
+
if (result.authorization?.present)
|
|
994
|
+
seen.push(`Authorization: ${result.authorization.redacted}${result.authorization.bearerPrefix === false ? ' (no Bearer prefix)' : ''}`);
|
|
995
|
+
if (result.xApiKey?.present)
|
|
996
|
+
seen.push(`x-api-key: ${result.xApiKey.redacted}`);
|
|
997
|
+
console.log(` Received: ${seen.length > 0 ? seen.join(', ') : 'no auth headers'}`);
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
console.log(` Received: (no request within ${Math.round(timeoutMs / 1000)}s)`);
|
|
1001
|
+
}
|
|
1002
|
+
console.log('');
|
|
1003
|
+
console.log(' ' + result.diagnosis);
|
|
1004
|
+
console.log('');
|
|
1005
|
+
process.exit(result.verdict === 'match' ? 0 : 1);
|
|
1006
|
+
}
|
|
1007
|
+
const checks = await runChecks({ probe });
|
|
1008
|
+
if (asJson) {
|
|
1009
|
+
// JSON mode is meant for machine consumption (claude-bridge /status,
|
|
1010
|
+
// deepdive health checks, CI scripts) — no decorative header, no
|
|
1011
|
+
// trailing prose, stable shape. Exit code is also surfaced in the
|
|
1012
|
+
// JSON envelope for callers that can't read process exit codes.
|
|
1013
|
+
process.stdout.write(formatChecksJson(checks) + '\n');
|
|
1014
|
+
process.exit(exitCodeFor(checks));
|
|
1015
|
+
}
|
|
952
1016
|
console.log('');
|
|
953
1017
|
console.log(' dario — Doctor');
|
|
954
1018
|
console.log(' ─────────────');
|
|
955
1019
|
console.log('');
|
|
956
|
-
const checks = await runChecks({ probe });
|
|
957
1020
|
console.log(formatChecks(checks));
|
|
958
1021
|
console.log('');
|
|
959
1022
|
const code = exitCodeFor(checks);
|
|
@@ -978,6 +1041,86 @@ async function version() {
|
|
|
978
1041
|
console.log('unknown');
|
|
979
1042
|
}
|
|
980
1043
|
}
|
|
1044
|
+
async function config() {
|
|
1045
|
+
const { collectEffectiveConfig, formatEffectiveConfig, formatEffectiveConfigJson } = await import('./config-report.js');
|
|
1046
|
+
const asJson = args.includes('--json');
|
|
1047
|
+
const report = await collectEffectiveConfig();
|
|
1048
|
+
if (asJson) {
|
|
1049
|
+
process.stdout.write(formatEffectiveConfigJson(report) + '\n');
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
console.log('');
|
|
1053
|
+
console.log(' dario — Config');
|
|
1054
|
+
console.log(' ─────────────');
|
|
1055
|
+
console.log('');
|
|
1056
|
+
console.log(formatEffectiveConfig(report));
|
|
1057
|
+
}
|
|
1058
|
+
async function upgrade() {
|
|
1059
|
+
// Thin wrapper over `npm install -g @askalf/dario@latest`. The value
|
|
1060
|
+
// isn't in saving the user typing — it's in the pre-flight (print
|
|
1061
|
+
// current vs. latest version, refuse to run if already on latest,
|
|
1062
|
+
// fail with a clear hint if npm is missing).
|
|
1063
|
+
const { spawnSync } = await import('node:child_process');
|
|
1064
|
+
const { fileURLToPath } = await import('node:url');
|
|
1065
|
+
const { readFile: rf } = await import('node:fs/promises');
|
|
1066
|
+
const dir = join(fileURLToPath(import.meta.url), '..', '..');
|
|
1067
|
+
let currentVersion = 'unknown';
|
|
1068
|
+
try {
|
|
1069
|
+
const pkg = JSON.parse(await rf(join(dir, 'package.json'), 'utf-8'));
|
|
1070
|
+
currentVersion = pkg.version ?? 'unknown';
|
|
1071
|
+
}
|
|
1072
|
+
catch { /* noop */ }
|
|
1073
|
+
console.log('');
|
|
1074
|
+
console.log(' dario — Upgrade');
|
|
1075
|
+
console.log(' ──────────────');
|
|
1076
|
+
console.log('');
|
|
1077
|
+
console.log(` Current: v${currentVersion}`);
|
|
1078
|
+
// Probe npm for the latest version first — avoids a long npm install if
|
|
1079
|
+
// the user's already on @latest. 3s timeout keeps the pre-flight short.
|
|
1080
|
+
let latestVersion = null;
|
|
1081
|
+
try {
|
|
1082
|
+
const res = spawnSync('npm', ['view', '@askalf/dario', 'version'], {
|
|
1083
|
+
encoding: 'utf8',
|
|
1084
|
+
timeout: 3000,
|
|
1085
|
+
shell: process.platform === 'win32',
|
|
1086
|
+
});
|
|
1087
|
+
if (res.status === 0) {
|
|
1088
|
+
const m = /(\d+\.\d+\.\d+(?:[.\-][\w.\-]+)?)/.exec(res.stdout);
|
|
1089
|
+
latestVersion = m ? m[1] : null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* noop */ }
|
|
1093
|
+
if (!latestVersion) {
|
|
1094
|
+
console.log(' Latest: (npm view failed — is npm on PATH? Continuing anyway.)');
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
console.log(` Latest: v${latestVersion}`);
|
|
1098
|
+
if (latestVersion === currentVersion) {
|
|
1099
|
+
console.log('');
|
|
1100
|
+
console.log(' Already on the latest release. Nothing to do.');
|
|
1101
|
+
console.log('');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
console.log('');
|
|
1106
|
+
console.log(' Running: npm install -g @askalf/dario@latest');
|
|
1107
|
+
console.log('');
|
|
1108
|
+
const install = spawnSync('npm', ['install', '-g', '@askalf/dario@latest'], {
|
|
1109
|
+
stdio: 'inherit',
|
|
1110
|
+
shell: process.platform === 'win32',
|
|
1111
|
+
});
|
|
1112
|
+
if (install.status !== 0) {
|
|
1113
|
+
console.error('');
|
|
1114
|
+
console.error(' npm install failed. If this is a permissions issue, try:');
|
|
1115
|
+
console.error(' sudo npm install -g @askalf/dario@latest # POSIX');
|
|
1116
|
+
console.error(' npm install -g @askalf/dario@latest # Windows (may need admin)');
|
|
1117
|
+
console.error('');
|
|
1118
|
+
process.exit(install.status ?? 1);
|
|
1119
|
+
}
|
|
1120
|
+
console.log('');
|
|
1121
|
+
console.log(' Upgrade complete. Run `dario --version` to confirm.');
|
|
1122
|
+
console.log('');
|
|
1123
|
+
}
|
|
981
1124
|
// Main
|
|
982
1125
|
const commands = {
|
|
983
1126
|
login,
|
|
@@ -991,6 +1134,8 @@ const commands = {
|
|
|
991
1134
|
subagent,
|
|
992
1135
|
mcp,
|
|
993
1136
|
doctor,
|
|
1137
|
+
config,
|
|
1138
|
+
upgrade,
|
|
994
1139
|
help,
|
|
995
1140
|
version,
|
|
996
1141
|
'--help': help,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dario config` — print effective configuration with credentials redacted.
|
|
3
|
+
*
|
|
4
|
+
* Different from `dario doctor`: doctor is "is it working?", config is
|
|
5
|
+
* "what IS it?". The overlap is intentional — config shows *settings*
|
|
6
|
+
* (port, host, DARIO_API_KEY state, model defaults) that operators
|
|
7
|
+
* need to confirm when debugging a client misconfiguration. Doctor
|
|
8
|
+
* shows *health* (OAuth expiry, template drift, TLS fingerprint
|
|
9
|
+
* match) that operators need to confirm when debugging a routing
|
|
10
|
+
* failure.
|
|
11
|
+
*
|
|
12
|
+
* Every output row is already safe to paste into a bug report:
|
|
13
|
+
* credentials are replaced with `set`/`unset` state tags, paths are
|
|
14
|
+
* left untouched because they're operationally useful, tokens never
|
|
15
|
+
* appear.
|
|
16
|
+
*/
|
|
17
|
+
export interface ConfigSection {
|
|
18
|
+
title: string;
|
|
19
|
+
rows: Array<{
|
|
20
|
+
label: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ConfigReport {
|
|
25
|
+
generatedAt: string;
|
|
26
|
+
version: string;
|
|
27
|
+
sections: ConfigSection[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Collect the effective dario configuration the proxy would run with.
|
|
31
|
+
* Reads env vars, filesystem state (credentials, override files, caches),
|
|
32
|
+
* account pool, and configured backends. Never reads the actual
|
|
33
|
+
* credential VALUES — only their presence/absence/path.
|
|
34
|
+
*/
|
|
35
|
+
export declare function collectEffectiveConfig(): Promise<ConfigReport>;
|
|
36
|
+
export declare function formatAge(ms: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Pretty-print a ConfigReport as aligned ASCII. Same approach as
|
|
39
|
+
* doctor's formatChecks — plain text, no colors, pasteable.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatEffectiveConfig(report: ConfigReport): string;
|
|
42
|
+
/** Structured envelope for `dario config --json`. */
|
|
43
|
+
export declare function formatEffectiveConfigJson(report: ConfigReport): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dario config` — print effective configuration with credentials redacted.
|
|
3
|
+
*
|
|
4
|
+
* Different from `dario doctor`: doctor is "is it working?", config is
|
|
5
|
+
* "what IS it?". The overlap is intentional — config shows *settings*
|
|
6
|
+
* (port, host, DARIO_API_KEY state, model defaults) that operators
|
|
7
|
+
* need to confirm when debugging a client misconfiguration. Doctor
|
|
8
|
+
* shows *health* (OAuth expiry, template drift, TLS fingerprint
|
|
9
|
+
* match) that operators need to confirm when debugging a routing
|
|
10
|
+
* failure.
|
|
11
|
+
*
|
|
12
|
+
* Every output row is already safe to paste into a bug report:
|
|
13
|
+
* credentials are replaced with `set`/`unset` state tags, paths are
|
|
14
|
+
* left untouched because they're operationally useful, tokens never
|
|
15
|
+
* appear.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
/**
|
|
23
|
+
* Collect the effective dario configuration the proxy would run with.
|
|
24
|
+
* Reads env vars, filesystem state (credentials, override files, caches),
|
|
25
|
+
* account pool, and configured backends. Never reads the actual
|
|
26
|
+
* credential VALUES — only their presence/absence/path.
|
|
27
|
+
*/
|
|
28
|
+
export async function collectEffectiveConfig() {
|
|
29
|
+
const sections = [];
|
|
30
|
+
const home = join(homedir(), '.dario');
|
|
31
|
+
// ── Identity
|
|
32
|
+
let version = 'unknown';
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
35
|
+
version = pkg.version ?? 'unknown';
|
|
36
|
+
}
|
|
37
|
+
catch { /* noop */ }
|
|
38
|
+
sections.push({
|
|
39
|
+
title: 'Identity',
|
|
40
|
+
rows: [
|
|
41
|
+
{ label: 'version', value: `v${version}` },
|
|
42
|
+
{ label: 'runtime', value: `node ${process.version} on ${process.platform} ${process.arch}` },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
// ── Proxy bind
|
|
46
|
+
sections.push({
|
|
47
|
+
title: 'Proxy (on `dario proxy`)',
|
|
48
|
+
rows: [
|
|
49
|
+
{ label: 'port', value: envOrDefault('DARIO_PORT', '3456') },
|
|
50
|
+
{ label: 'host', value: envOrDefault('DARIO_HOST', '127.0.0.1') },
|
|
51
|
+
{ label: 'model', value: envOrDefault('DARIO_MODEL', '(passthrough — client picks)') },
|
|
52
|
+
{ label: 'effort', value: envOrDefault('DARIO_EFFORT', '(CC default)') },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
// ── Auth gate
|
|
56
|
+
sections.push({
|
|
57
|
+
title: 'Auth gate',
|
|
58
|
+
rows: [
|
|
59
|
+
{
|
|
60
|
+
label: 'DARIO_API_KEY',
|
|
61
|
+
value: process.env.DARIO_API_KEY
|
|
62
|
+
? `set (length ${process.env.DARIO_API_KEY.length}) — x-api-key / Authorization Bearer required`
|
|
63
|
+
: 'unset — auth not enforced on loopback',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'DARIO_STRICT_TLS',
|
|
67
|
+
value: process.env.DARIO_STRICT_TLS === '1' ? 'on' : 'off',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
// ── OAuth (Claude subscription credentials)
|
|
72
|
+
const credsPath = join(home, 'credentials.json');
|
|
73
|
+
const credsInfo = describeCreds(credsPath);
|
|
74
|
+
sections.push({
|
|
75
|
+
title: 'OAuth',
|
|
76
|
+
rows: [
|
|
77
|
+
{ label: 'credentials', value: credsInfo },
|
|
78
|
+
{ label: 'path', value: credsPath },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
// ── Pool
|
|
82
|
+
try {
|
|
83
|
+
const { listAccountAliases } = await import('./accounts.js');
|
|
84
|
+
const aliases = await listAccountAliases();
|
|
85
|
+
sections.push({
|
|
86
|
+
title: 'Account pool',
|
|
87
|
+
rows: [
|
|
88
|
+
{ label: 'mode', value: aliases.length === 0 ? 'single-account (no pool)' : `pool of ${aliases.length}` },
|
|
89
|
+
...(aliases.length > 0 ? [{ label: 'aliases', value: aliases.join(', ') }] : []),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
sections.push({
|
|
95
|
+
title: 'Account pool',
|
|
96
|
+
rows: [{ label: 'mode', value: '(check failed)' }],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// ── Backends
|
|
100
|
+
try {
|
|
101
|
+
const { listBackends } = await import('./openai-backend.js');
|
|
102
|
+
const backends = await listBackends();
|
|
103
|
+
sections.push({
|
|
104
|
+
title: 'OpenAI-compat backends',
|
|
105
|
+
rows: [
|
|
106
|
+
{ label: 'count', value: String(backends.length) },
|
|
107
|
+
...(backends.length > 0
|
|
108
|
+
? [{ label: 'names', value: backends.map((b) => b.name).join(', ') }]
|
|
109
|
+
: []),
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
sections.push({
|
|
115
|
+
title: 'OpenAI-compat backends',
|
|
116
|
+
rows: [{ label: 'count', value: '(check failed)' }],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── Paths (everything dario reads/writes on disk)
|
|
120
|
+
sections.push({
|
|
121
|
+
title: 'Paths',
|
|
122
|
+
rows: [
|
|
123
|
+
{ label: 'home', value: home },
|
|
124
|
+
{ label: 'credentials', value: credsPath },
|
|
125
|
+
{ label: 'accounts', value: join(home, 'accounts') },
|
|
126
|
+
{ label: 'oauth cache', value: join(home, 'cc-oauth-cache-v6.json') },
|
|
127
|
+
{ label: 'oauth override', value: join(home, 'oauth-config.override.json') },
|
|
128
|
+
{ label: 'template cache', value: join(home, 'template-cache.json') },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
generatedAt: new Date().toISOString(),
|
|
133
|
+
version,
|
|
134
|
+
sections,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function envOrDefault(name, dflt) {
|
|
138
|
+
return process.env[name] ? `${process.env[name]} (from ${name})` : dflt;
|
|
139
|
+
}
|
|
140
|
+
function describeCreds(path) {
|
|
141
|
+
if (!existsSync(path))
|
|
142
|
+
return 'not authenticated (run `dario login`)';
|
|
143
|
+
try {
|
|
144
|
+
const s = statSync(path);
|
|
145
|
+
const mode = (s.mode & 0o777).toString(8);
|
|
146
|
+
const age = formatAge(Date.now() - s.mtimeMs);
|
|
147
|
+
return `present (mode ${mode}, last updated ${age} ago)`;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return 'present (stat failed)';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Exported for unit tests.
|
|
154
|
+
export function formatAge(ms) {
|
|
155
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
156
|
+
if (s < 60)
|
|
157
|
+
return `${s}s`;
|
|
158
|
+
const m = Math.round(s / 60);
|
|
159
|
+
if (m < 60)
|
|
160
|
+
return `${m}m`;
|
|
161
|
+
const h = Math.round(m / 60);
|
|
162
|
+
if (h < 24)
|
|
163
|
+
return `${h}h`;
|
|
164
|
+
const d = Math.round(h / 24);
|
|
165
|
+
return `${d}d`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Pretty-print a ConfigReport as aligned ASCII. Same approach as
|
|
169
|
+
* doctor's formatChecks — plain text, no colors, pasteable.
|
|
170
|
+
*/
|
|
171
|
+
export function formatEffectiveConfig(report) {
|
|
172
|
+
const lines = [];
|
|
173
|
+
for (const section of report.sections) {
|
|
174
|
+
lines.push(` ${section.title}`);
|
|
175
|
+
lines.push(` ${'─'.repeat(section.title.length)}`);
|
|
176
|
+
const labelWidth = section.rows.reduce((n, r) => Math.max(n, r.label.length), 0);
|
|
177
|
+
for (const r of section.rows) {
|
|
178
|
+
lines.push(` ${r.label.padEnd(labelWidth)} ${r.value}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push('');
|
|
181
|
+
}
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
/** Structured envelope for `dario config --json`. */
|
|
185
|
+
export function formatEffectiveConfigJson(report) {
|
|
186
|
+
return JSON.stringify(report, null, 2);
|
|
187
|
+
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ 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
|
+
/**
|
|
35
|
+
* Serialize a check report as structured JSON. Lets other tools
|
|
36
|
+
* (claude-bridge's /status command, deepdive, CI scripts) consume
|
|
37
|
+
* dario's health programmatically instead of scraping the formatted
|
|
38
|
+
* text. Emitted by `dario doctor --json`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatChecksJson(checks: Check[]): string;
|
|
41
|
+
export declare function probeNpmLatestCC(): string | null;
|
|
34
42
|
export interface RunChecksOptions {
|
|
35
43
|
/**
|
|
36
44
|
* Opt-in: hit Anthropic's authorize endpoint with the scope set dario
|
|
@@ -53,3 +61,48 @@ export interface RunChecksOptions {
|
|
|
53
61
|
* the environment before the subsystems.
|
|
54
62
|
*/
|
|
55
63
|
export declare function runChecks(opts?: RunChecksOptions): Promise<Check[]>;
|
|
64
|
+
export interface SeenHeader {
|
|
65
|
+
present: boolean;
|
|
66
|
+
/** Redacted preview: `"abcd...wxyz"` — first 4 + last 4 chars, or length tag if the value is too short to excerpt safely. */
|
|
67
|
+
redacted?: string;
|
|
68
|
+
length?: number;
|
|
69
|
+
/** For Authorization: whether the value started with "Bearer " (case-insensitive). */
|
|
70
|
+
bearerPrefix?: boolean;
|
|
71
|
+
/** Did this header's value (after any `Bearer ` strip) match DARIO_API_KEY? */
|
|
72
|
+
matches?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export type AuthCheckVerdict = 'match' | 'mismatch' | 'no-auth-header' | 'timeout' | 'no-enforcement';
|
|
75
|
+
export interface AuthCheckResult {
|
|
76
|
+
received: boolean;
|
|
77
|
+
port?: number;
|
|
78
|
+
expected: string;
|
|
79
|
+
xApiKey?: SeenHeader;
|
|
80
|
+
authorization?: SeenHeader;
|
|
81
|
+
verdict: AuthCheckVerdict;
|
|
82
|
+
diagnosis: string;
|
|
83
|
+
}
|
|
84
|
+
export interface AuthCheckOptions {
|
|
85
|
+
/** Milliseconds to wait for an inbound request. Default 30,000. */
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
/** Override the expected key. Default: `process.env.DARIO_API_KEY`. */
|
|
88
|
+
expectedKey?: string;
|
|
89
|
+
/** Test hook: called when the server is listening, with the port. */
|
|
90
|
+
onListening?: (port: number) => void;
|
|
91
|
+
}
|
|
92
|
+
export declare function redactSecret(value: string): string;
|
|
93
|
+
export declare function classifyAuthHeaders(headers: {
|
|
94
|
+
'x-api-key'?: string | string[];
|
|
95
|
+
authorization?: string | string[];
|
|
96
|
+
}, expected: string): {
|
|
97
|
+
xApiKey: SeenHeader;
|
|
98
|
+
authorization: SeenHeader;
|
|
99
|
+
verdict: AuthCheckVerdict;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Listen for one inbound request on a random loopback port, classify
|
|
103
|
+
* whatever auth headers it carries against `DARIO_API_KEY`, return a
|
|
104
|
+
* structured result. Sends 200 / 401 to the inbound request so the
|
|
105
|
+
* client doesn't hang, then closes. This is a probe — it does not
|
|
106
|
+
* proxy, does not log, does not persist.
|
|
107
|
+
*/
|
|
108
|
+
export declare function runAuthCheck(opts?: AuthCheckOptions): Promise<AuthCheckResult>;
|
package/dist/doctor.js
CHANGED
|
@@ -14,8 +14,10 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import { join, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { homedir, platform, arch, release } from 'node:os';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { createServer } from 'node:http';
|
|
17
19
|
import { CC_TEMPLATE, } from './cc-template.js';
|
|
18
|
-
import { describeTemplate, detectDrift, checkCCCompat, findInstalledCC, SUPPORTED_CC_RANGE, CURRENT_SCHEMA_VERSION, } from './live-fingerprint.js';
|
|
20
|
+
import { describeTemplate, detectDrift, checkCCCompat, findInstalledCC, SUPPORTED_CC_RANGE, CURRENT_SCHEMA_VERSION, compareVersions, } from './live-fingerprint.js';
|
|
19
21
|
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
20
22
|
import { runAuthorizeProbe } from './cc-authorize-probe.js';
|
|
21
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -43,6 +45,63 @@ export function formatChecks(checks) {
|
|
|
43
45
|
export function exitCodeFor(checks) {
|
|
44
46
|
return checks.some((c) => c.status === 'fail') ? 1 : 0;
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Serialize a check report as structured JSON. Lets other tools
|
|
50
|
+
* (claude-bridge's /status command, deepdive, CI scripts) consume
|
|
51
|
+
* dario's health programmatically instead of scraping the formatted
|
|
52
|
+
* text. Emitted by `dario doctor --json`.
|
|
53
|
+
*/
|
|
54
|
+
export function formatChecksJson(checks) {
|
|
55
|
+
const summary = {
|
|
56
|
+
ok: checks.filter((c) => c.status === 'ok').length,
|
|
57
|
+
warn: checks.filter((c) => c.status === 'warn').length,
|
|
58
|
+
fail: checks.filter((c) => c.status === 'fail').length,
|
|
59
|
+
info: checks.filter((c) => c.status === 'info').length,
|
|
60
|
+
};
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
generatedAt: new Date().toISOString(),
|
|
63
|
+
exitCode: exitCodeFor(checks),
|
|
64
|
+
summary,
|
|
65
|
+
checks,
|
|
66
|
+
}, null, 2);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ask npm for the latest @anthropic-ai/claude-code version. One 3s
|
|
70
|
+
* timeout; failures return null so doctor silently drops the check.
|
|
71
|
+
* Result is cached module-scoped so back-to-back doctor invocations
|
|
72
|
+
* (e.g. from a wrapping script) don't hammer the npm registry.
|
|
73
|
+
*/
|
|
74
|
+
let _npmLatestCache = null;
|
|
75
|
+
const NPM_CACHE_TTL_MS = 60 * 1000;
|
|
76
|
+
export function probeNpmLatestCC() {
|
|
77
|
+
if (_npmLatestCache && Date.now() - _npmLatestCache.at < NPM_CACHE_TTL_MS) {
|
|
78
|
+
return _npmLatestCache.value;
|
|
79
|
+
}
|
|
80
|
+
let value = null;
|
|
81
|
+
try {
|
|
82
|
+
// `npm view <pkg> version` prints the version as a single line.
|
|
83
|
+
// 3s timeout keeps doctor responsive even with flaky network /
|
|
84
|
+
// corporate proxies; stdio ignores stderr so "npm notice" banners
|
|
85
|
+
// don't pollute stdout parsing.
|
|
86
|
+
const out = execFileSync('npm', ['view', '@anthropic-ai/claude-code', 'version'], {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
timeout: 3_000,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
// npm ships as .cmd on Windows; execFile can't spawn it directly
|
|
92
|
+
// without shell:true. `npm` is not user-overridable here so the
|
|
93
|
+
// command-injection risk is nil.
|
|
94
|
+
shell: process.platform === 'win32',
|
|
95
|
+
});
|
|
96
|
+
const m = /(\d+\.\d+\.\d+(?:[.\-][\w.\-]+)?)/.exec(out);
|
|
97
|
+
value = m ? m[1] : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
value = null;
|
|
101
|
+
}
|
|
102
|
+
_npmLatestCache = { value, at: Date.now() };
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
46
105
|
/**
|
|
47
106
|
* Run every available health check. Never throws — each check is
|
|
48
107
|
* individually try/caught so a broken subsystem (e.g. unreadable accounts
|
|
@@ -107,6 +166,24 @@ export async function runChecks(opts = {}) {
|
|
|
107
166
|
label: 'CC binary',
|
|
108
167
|
detail: `v${cc.version} at ${cc.path} (range: v${SUPPORTED_CC_RANGE.min} – v${SUPPORTED_CC_RANGE.maxTested})`,
|
|
109
168
|
});
|
|
169
|
+
// Stale-upstream probe: compare installed against npm's @latest.
|
|
170
|
+
// One network hop (3s timeout, 60s in-process cache). Silent on
|
|
171
|
+
// failure — no check row emitted — since a flaky network
|
|
172
|
+
// shouldn't turn doctor's output noisy. Only emits when the
|
|
173
|
+
// installed CC is strictly older than the npm latest.
|
|
174
|
+
try {
|
|
175
|
+
const npmLatest = probeNpmLatestCC();
|
|
176
|
+
if (npmLatest && compareVersions(cc.version, npmLatest) < 0) {
|
|
177
|
+
checks.push({
|
|
178
|
+
status: 'info',
|
|
179
|
+
label: 'CC upstream',
|
|
180
|
+
detail: `npm latest is v${npmLatest} — installed is v${cc.version}. ` +
|
|
181
|
+
`Run \`npm install -g @anthropic-ai/claude-code@latest\` to upgrade; ` +
|
|
182
|
+
`dario's template will re-capture automatically on next startup.`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* silent */ }
|
|
110
187
|
}
|
|
111
188
|
else if (cc.path) {
|
|
112
189
|
checks.push({
|
|
@@ -296,3 +373,160 @@ function safely(fn, fallback) {
|
|
|
296
373
|
return fallback;
|
|
297
374
|
}
|
|
298
375
|
}
|
|
376
|
+
// Exported for unit tests.
|
|
377
|
+
export function redactSecret(value) {
|
|
378
|
+
if (value.length <= 8)
|
|
379
|
+
return `<${value.length} chars>`;
|
|
380
|
+
return `${value.slice(0, 4)}…${value.slice(-4)} (length ${value.length})`;
|
|
381
|
+
}
|
|
382
|
+
// Exported for unit tests. Pure — takes the two headers + the expected
|
|
383
|
+
// key, returns the classification. Separated from the HTTP dance so
|
|
384
|
+
// tests can drive synthetic inputs without binding a socket.
|
|
385
|
+
export function classifyAuthHeaders(headers, expected) {
|
|
386
|
+
const xRaw = headers['x-api-key'];
|
|
387
|
+
const aRaw = headers['authorization'];
|
|
388
|
+
const xVal = Array.isArray(xRaw) ? xRaw[0] : xRaw;
|
|
389
|
+
const aVal = Array.isArray(aRaw) ? aRaw[0] : aRaw;
|
|
390
|
+
const xApiKey = { present: xVal !== undefined };
|
|
391
|
+
if (xVal !== undefined) {
|
|
392
|
+
xApiKey.length = xVal.length;
|
|
393
|
+
xApiKey.redacted = redactSecret(xVal);
|
|
394
|
+
xApiKey.matches = xVal === expected;
|
|
395
|
+
}
|
|
396
|
+
const authorization = { present: aVal !== undefined };
|
|
397
|
+
if (aVal !== undefined) {
|
|
398
|
+
authorization.length = aVal.length;
|
|
399
|
+
authorization.bearerPrefix = /^Bearer\s+/i.test(aVal);
|
|
400
|
+
const stripped = aVal.replace(/^Bearer\s+/i, '');
|
|
401
|
+
authorization.redacted = redactSecret(stripped);
|
|
402
|
+
authorization.matches = stripped === expected;
|
|
403
|
+
}
|
|
404
|
+
let verdict;
|
|
405
|
+
if (!xApiKey.present && !authorization.present) {
|
|
406
|
+
verdict = 'no-auth-header';
|
|
407
|
+
}
|
|
408
|
+
else if (xApiKey.matches === true || authorization.matches === true) {
|
|
409
|
+
verdict = 'match';
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
verdict = 'mismatch';
|
|
413
|
+
}
|
|
414
|
+
return { xApiKey, authorization, verdict };
|
|
415
|
+
}
|
|
416
|
+
function diagnoseAuthCheck(result) {
|
|
417
|
+
switch (result.verdict) {
|
|
418
|
+
case 'match':
|
|
419
|
+
return `client auth matches DARIO_API_KEY. A real dario proxy would accept this request.`;
|
|
420
|
+
case 'mismatch': {
|
|
421
|
+
const parts = [];
|
|
422
|
+
if (result.authorization?.present) {
|
|
423
|
+
const bearer = result.authorization.bearerPrefix ? ' (Bearer prefix present)' : ' (Bearer prefix missing)';
|
|
424
|
+
parts.push(`Authorization header${bearer}: value ${result.authorization.redacted} — does NOT match expected ${redactSecret(result.expected)}.`);
|
|
425
|
+
}
|
|
426
|
+
if (result.xApiKey?.present) {
|
|
427
|
+
parts.push(`x-api-key header: value ${result.xApiKey.redacted} — does NOT match expected ${redactSecret(result.expected)}.`);
|
|
428
|
+
}
|
|
429
|
+
const hint = suggestAuthFix(result);
|
|
430
|
+
return parts.join(' ') + (hint ? ' ' + hint : '');
|
|
431
|
+
}
|
|
432
|
+
case 'no-auth-header':
|
|
433
|
+
return (`client sent no x-api-key and no Authorization header. ` +
|
|
434
|
+
`Expected ${redactSecret(result.expected)} in either. ` +
|
|
435
|
+
`Set ANTHROPIC_API_KEY=${result.expected} in your client's environment, ` +
|
|
436
|
+
`or use your tool's own "API key" config field if it has one.`);
|
|
437
|
+
case 'timeout':
|
|
438
|
+
return (`no request received within the timeout. Did your client target the ` +
|
|
439
|
+
`port printed above? If the client uses a base URL you configured ` +
|
|
440
|
+
`elsewhere, point it at the --auth-check listener for this one request.`);
|
|
441
|
+
case 'no-enforcement':
|
|
442
|
+
return (`DARIO_API_KEY is not set — dario does not enforce auth on loopback ` +
|
|
443
|
+
`by default, so any request would be allowed through. To test auth ` +
|
|
444
|
+
`enforcement, set DARIO_API_KEY=your-secret before running --auth-check.`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Pattern-match common failure modes for a sharper hint. */
|
|
448
|
+
function suggestAuthFix(result) {
|
|
449
|
+
const auth = result.authorization;
|
|
450
|
+
const exp = result.expected;
|
|
451
|
+
// Authorization value looks like a real Anthropic key — very common
|
|
452
|
+
// pattern: client has one stashed from an earlier setup (OpenClaw's
|
|
453
|
+
// auth-profiles.json, ANTHROPIC_API_KEY env, config file) and it's
|
|
454
|
+
// shadowing the intended "dario" value. dario#97 exactly.
|
|
455
|
+
if (auth?.present && auth.redacted?.startsWith('sk-a')) {
|
|
456
|
+
return (`The value your client sent looks like a real Anthropic API key ` +
|
|
457
|
+
`(starts with "sk-a…"). Your client has that key configured somewhere ` +
|
|
458
|
+
`(auth-profiles.json, ANTHROPIC_API_KEY env, client config file) and it's ` +
|
|
459
|
+
`overriding "${exp}". Either replace it with "${exp}" or bypass auth entirely ` +
|
|
460
|
+
`by running dario on --host=127.0.0.1 without DARIO_API_KEY set.`);
|
|
461
|
+
}
|
|
462
|
+
// Authorization with no "Bearer " prefix: some clients just set the
|
|
463
|
+
// header value raw.
|
|
464
|
+
if (auth?.present && auth.bearerPrefix === false) {
|
|
465
|
+
return (`The Authorization header is missing the "Bearer " prefix. Most ` +
|
|
466
|
+
`HTTP client libraries want "Bearer <key>" as one value — yours seems ` +
|
|
467
|
+
`to be setting the key directly. Check your client's auth config.`);
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Listen for one inbound request on a random loopback port, classify
|
|
473
|
+
* whatever auth headers it carries against `DARIO_API_KEY`, return a
|
|
474
|
+
* structured result. Sends 200 / 401 to the inbound request so the
|
|
475
|
+
* client doesn't hang, then closes. This is a probe — it does not
|
|
476
|
+
* proxy, does not log, does not persist.
|
|
477
|
+
*/
|
|
478
|
+
export async function runAuthCheck(opts = {}) {
|
|
479
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
480
|
+
const expected = opts.expectedKey ?? process.env.DARIO_API_KEY ?? '';
|
|
481
|
+
if (!expected) {
|
|
482
|
+
return {
|
|
483
|
+
received: false,
|
|
484
|
+
expected: '<unset>',
|
|
485
|
+
verdict: 'no-enforcement',
|
|
486
|
+
diagnosis: diagnoseAuthCheck({ received: false, expected: '<unset>', verdict: 'no-enforcement' }),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return new Promise((resolve) => {
|
|
490
|
+
let settled = false;
|
|
491
|
+
const settle = (result) => {
|
|
492
|
+
if (settled)
|
|
493
|
+
return;
|
|
494
|
+
settled = true;
|
|
495
|
+
server.close(() => resolve(result));
|
|
496
|
+
};
|
|
497
|
+
const server = createServer((req, res) => {
|
|
498
|
+
const { xApiKey, authorization, verdict } = classifyAuthHeaders(req.headers, expected);
|
|
499
|
+
const port = server.address()?.port;
|
|
500
|
+
const result = {
|
|
501
|
+
received: true,
|
|
502
|
+
port,
|
|
503
|
+
expected,
|
|
504
|
+
xApiKey,
|
|
505
|
+
authorization,
|
|
506
|
+
verdict,
|
|
507
|
+
diagnosis: '',
|
|
508
|
+
};
|
|
509
|
+
result.diagnosis = diagnoseAuthCheck(result);
|
|
510
|
+
res.writeHead(verdict === 'match' ? 200 : 401, { 'content-type': 'application/json' });
|
|
511
|
+
res.end(JSON.stringify({
|
|
512
|
+
message: 'dario auth-check received this request — see the dario CLI output for the diagnostic.',
|
|
513
|
+
verdict,
|
|
514
|
+
}));
|
|
515
|
+
settle(result);
|
|
516
|
+
});
|
|
517
|
+
server.listen(0, '127.0.0.1', () => {
|
|
518
|
+
const port = server.address().port;
|
|
519
|
+
opts.onListening?.(port);
|
|
520
|
+
});
|
|
521
|
+
setTimeout(() => {
|
|
522
|
+
const port = server.address()?.port;
|
|
523
|
+
settle({
|
|
524
|
+
received: false,
|
|
525
|
+
port,
|
|
526
|
+
expected,
|
|
527
|
+
verdict: 'timeout',
|
|
528
|
+
diagnosis: diagnoseAuthCheck({ received: false, port, expected, verdict: 'timeout' }),
|
|
529
|
+
});
|
|
530
|
+
}, timeoutMs);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
|
|
|
282
282
|
*/
|
|
283
283
|
export declare const SUPPORTED_CC_RANGE: {
|
|
284
284
|
readonly min: "1.0.0";
|
|
285
|
-
readonly maxTested: "2.1.
|
|
285
|
+
readonly maxTested: "2.1.119";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -777,7 +777,7 @@ export function _resetInstalledVersionProbeForTest() {
|
|
|
777
777
|
*/
|
|
778
778
|
export const SUPPORTED_CC_RANGE = {
|
|
779
779
|
min: '1.0.0',
|
|
780
|
-
maxTested: '2.1.
|
|
780
|
+
maxTested: '2.1.119',
|
|
781
781
|
};
|
|
782
782
|
/**
|
|
783
783
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.31.
|
|
3
|
+
"version": "3.31.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": {
|