@askalf/dario 4.8.119 → 4.8.120
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/health-response.d.ts +19 -1
- package/dist/health-response.js +34 -8
- package/dist/proxy.js +31 -5
- package/package.json +1 -1
|
@@ -41,4 +41,22 @@ export interface PoolDerivedStatus {
|
|
|
41
41
|
accounts: number;
|
|
42
42
|
}
|
|
43
43
|
export declare function derivePoolStatus(accounts: readonly PoolAccountStatusLike[], now: number, adminEnabled: boolean): PoolDerivedStatus;
|
|
44
|
-
export declare function buildHealthResponse(s: HealthStatusLike, requestCount: number,
|
|
44
|
+
export declare function buildHealthResponse(s: HealthStatusLike, requestCount: number, includeInternal: boolean): HealthResponse;
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether a /health caller may see the OAuth internals (#642).
|
|
47
|
+
*
|
|
48
|
+
* /health is intentionally auth-free (docker healthchecks need it before a
|
|
49
|
+
* key is configured), so we cannot simply gate on the API key. Trust model:
|
|
50
|
+
* - authenticated (valid DARIO_API_KEY) -> internal (an internal caller)
|
|
51
|
+
* - came via the Cloudflare tunnel (cf-ray) -> public (world-reachable)
|
|
52
|
+
* - otherwise bare loopback -> internal (docker HC / doctor)
|
|
53
|
+
* - otherwise (LAN, other container, WAN) -> public
|
|
54
|
+
* The cf-ray check is now only ever used to DENY (force public), never to
|
|
55
|
+
* grant, so spoofing it cannot widen disclosure — the previous fail-open
|
|
56
|
+
* direction is closed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function shouldDiscloseHealthInternals(opts: {
|
|
59
|
+
authenticated: boolean;
|
|
60
|
+
loopback: boolean;
|
|
61
|
+
viaCfRay: boolean;
|
|
62
|
+
}): boolean;
|
package/dist/health-response.js
CHANGED
|
@@ -59,24 +59,50 @@ export function derivePoolStatus(accounts, now, adminEnabled) {
|
|
|
59
59
|
expiresIn: formatMsLeft(earliest - now),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
export function buildHealthResponse(s, requestCount,
|
|
62
|
+
export function buildHealthResponse(s, requestCount, includeInternal) {
|
|
63
63
|
const dead = s.status === 'broken' ||
|
|
64
64
|
s.status === 'none' ||
|
|
65
65
|
(s.status === 'expired' && s.canRefresh === false);
|
|
66
66
|
const httpStatus = dead ? 503 : 200;
|
|
67
67
|
const liveness = { status: dead ? 'degraded' : 'ok' };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Only trusted callers (authenticated, or bare loopback not via the CF
|
|
69
|
+
// tunnel — see shouldDiscloseHealthInternals) get the OAuth internals.
|
|
70
|
+
// Everyone else (LAN, public tunnel) gets the liveness verdict only; the
|
|
71
|
+
// HTTP status is identical either way so uptime checks still work. #642:
|
|
72
|
+
// this used to key on the presence of the client-suppliable `cf-ray`
|
|
73
|
+
// header, which failed OPEN — a direct non-tunnel caller omits it and got
|
|
74
|
+
// the full internal view. `lastRefreshError` is no longer exposed here at
|
|
75
|
+
// all (it can carry a raw upstream error string); it remains on the
|
|
76
|
+
// key-gated /status.
|
|
77
|
+
const body = includeInternal
|
|
78
|
+
? {
|
|
71
79
|
...liveness,
|
|
72
|
-
// Version for internal callers only — an update-check signal (#640),
|
|
73
|
-
// withheld from the public-tunnel view alongside the OAuth internals.
|
|
74
80
|
...(s.version ? { version: s.version } : {}),
|
|
75
81
|
oauth: s.status,
|
|
76
82
|
expiresIn: s.expiresIn,
|
|
77
83
|
requests: requestCount,
|
|
78
84
|
...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
}
|
|
86
|
+
: liveness;
|
|
81
87
|
return { httpStatus, body };
|
|
82
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Decide whether a /health caller may see the OAuth internals (#642).
|
|
91
|
+
*
|
|
92
|
+
* /health is intentionally auth-free (docker healthchecks need it before a
|
|
93
|
+
* key is configured), so we cannot simply gate on the API key. Trust model:
|
|
94
|
+
* - authenticated (valid DARIO_API_KEY) -> internal (an internal caller)
|
|
95
|
+
* - came via the Cloudflare tunnel (cf-ray) -> public (world-reachable)
|
|
96
|
+
* - otherwise bare loopback -> internal (docker HC / doctor)
|
|
97
|
+
* - otherwise (LAN, other container, WAN) -> public
|
|
98
|
+
* The cf-ray check is now only ever used to DENY (force public), never to
|
|
99
|
+
* grant, so spoofing it cannot widen disclosure — the previous fail-open
|
|
100
|
+
* direction is closed.
|
|
101
|
+
*/
|
|
102
|
+
export function shouldDiscloseHealthInternals(opts) {
|
|
103
|
+
if (opts.authenticated)
|
|
104
|
+
return true;
|
|
105
|
+
if (opts.viaCfRay)
|
|
106
|
+
return false;
|
|
107
|
+
return opts.loopback;
|
|
108
|
+
}
|
package/dist/proxy.js
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { setDefaultResultOrder } from 'node:dns';
|
|
8
8
|
import { arch, platform } from 'node:process';
|
|
9
9
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
10
|
-
import { buildHealthResponse, derivePoolStatus } from './health-response.js';
|
|
10
|
+
import { buildHealthResponse, derivePoolStatus, shouldDiscloseHealthInternals } from './health-response.js';
|
|
11
11
|
import { darioVersion } from './version.js';
|
|
12
12
|
import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, CC_TEMPLATE } from './cc-template.js';
|
|
13
13
|
import { stampCch } from './cch.js';
|
|
@@ -38,6 +38,18 @@ function isLoopbackHost(host) {
|
|
|
38
38
|
return true;
|
|
39
39
|
return host.startsWith('127.');
|
|
40
40
|
}
|
|
41
|
+
// A socket peer address is loopback if it is 127.0.0.0/8 or ::1, including the
|
|
42
|
+
// IPv4-mapped-IPv6 form Node reports on dual-stack sockets (`::ffff:127.0.0.1`).
|
|
43
|
+
// Distinct from isLoopbackHost (which classifies a bind-address string): this
|
|
44
|
+
// classifies an inbound connection's peer for the /health disclosure gate (#642).
|
|
45
|
+
function isLoopbackAddr(addr) {
|
|
46
|
+
if (!addr)
|
|
47
|
+
return false;
|
|
48
|
+
if (addr === '::1')
|
|
49
|
+
return true;
|
|
50
|
+
const v4 = addr.startsWith('::ffff:') ? addr.slice(7) : addr;
|
|
51
|
+
return v4 === '127.0.0.1' || v4.startsWith('127.');
|
|
52
|
+
}
|
|
41
53
|
// Concurrency control: see src/request-queue.ts for the bounded queue
|
|
42
54
|
// (replaced the v3.30.x-and-earlier simple unbounded semaphore in dario#80).
|
|
43
55
|
// Billing tag hash seed — matches Claude Code's value
|
|
@@ -1169,6 +1181,14 @@ export async function startProxy(opts = {}) {
|
|
|
1169
1181
|
const adminTokenBuf = adminToken ? Buffer.from(adminToken) : null;
|
|
1170
1182
|
if (adminEnabled) {
|
|
1171
1183
|
console.log(`[dario] admin API enabled at /admin/* (token ${adminTokenBuf ? 'configured' : 'MISSING — endpoints return 403 until DARIO_ADMIN_TOKEN is set'})`);
|
|
1184
|
+
// Hardening (#642): the admin API adds/removes OAuth accounts. When it
|
|
1185
|
+
// shares DARIO_API_KEY (which is embedded in every client config) instead
|
|
1186
|
+
// of a distinct DARIO_ADMIN_TOKEN, every client that can proxy can also
|
|
1187
|
+
// control the account pool. Non-fatal for back-compat, but warn loudly.
|
|
1188
|
+
if (adminTokenBuf && !process.env.DARIO_ADMIN_TOKEN) {
|
|
1189
|
+
console.warn('[dario] WARNING: admin API is using DARIO_API_KEY as its token (no DARIO_ADMIN_TOKEN set).');
|
|
1190
|
+
console.warn('[dario] every client holding the proxy key can add/remove accounts. Set a distinct DARIO_ADMIN_TOKEN.');
|
|
1191
|
+
}
|
|
1172
1192
|
}
|
|
1173
1193
|
// Admin rate limiting (#620). Two token buckets, created once per proxy:
|
|
1174
1194
|
// failed auth (brute-force / broken-client throttle) and mutations. Global,
|
|
@@ -1269,10 +1289,16 @@ export async function startProxy(opts = {}) {
|
|
|
1269
1289
|
// react instead of cheerfully passing while every /v1/messages 401s.
|
|
1270
1290
|
if (urlPath === '/health' || urlPath === '/') {
|
|
1271
1291
|
const s = await currentStatus();
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1292
|
+
// Disclose OAuth internals only to trusted callers (#642). This used to
|
|
1293
|
+
// key on the presence of the client-suppliable `cf-ray` header and failed
|
|
1294
|
+
// OPEN — a direct non-tunnel caller omits it and got the full internal
|
|
1295
|
+
// view. Now: authenticated, OR bare loopback that did not arrive via CF.
|
|
1296
|
+
const includeInternal = shouldDiscloseHealthInternals({
|
|
1297
|
+
authenticated: authenticateRequest(req.headers, apiKeyBuf),
|
|
1298
|
+
loopback: isLoopbackAddr(req.socket?.remoteAddress),
|
|
1299
|
+
viaCfRay: req.headers['cf-ray'] !== undefined,
|
|
1300
|
+
});
|
|
1301
|
+
const { httpStatus, body } = buildHealthResponse({ ...s, version: darioVersion() }, requestCount, includeInternal);
|
|
1276
1302
|
res.writeHead(httpStatus, JSON_HEADERS);
|
|
1277
1303
|
res.end(JSON.stringify(body));
|
|
1278
1304
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.120",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|