@askalf/dario 4.8.105 → 4.8.106
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.d.ts +21 -0
- package/dist/accounts.js +35 -6
- package/dist/admin-api.d.ts +40 -0
- package/dist/admin-api.js +199 -0
- package/dist/proxy.js +29 -0
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -39,6 +39,27 @@ export declare function addAccountViaOAuth(alias: string): Promise<AccountCreden
|
|
|
39
39
|
* full URL to the browser.
|
|
40
40
|
*/
|
|
41
41
|
export declare function addAccountViaManualOAuth(alias: string): Promise<AccountCredentials>;
|
|
42
|
+
/**
|
|
43
|
+
* Non-interactive first half of the manual add-account flow (#599): validate
|
|
44
|
+
* the alias, generate PKCE + state, and build the authorize URL the user opens
|
|
45
|
+
* in a browser. The caller keeps `codeVerifier` + `state` and passes them back
|
|
46
|
+
* to `completeAddAccount` after the user supplies the displayed code. Shared by
|
|
47
|
+
* the `dario accounts add --manual` CLI and the headless admin API — the secret
|
|
48
|
+
* (codeVerifier) never leaves the process that started the flow.
|
|
49
|
+
*/
|
|
50
|
+
export declare function startAddAccount(alias: string): Promise<{
|
|
51
|
+
authorizeUrl: string;
|
|
52
|
+
codeVerifier: string;
|
|
53
|
+
state: string;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Non-interactive second half (#599): exchange the authorization `code` for
|
|
57
|
+
* tokens (PKCE-verified server-side), attach a device/account identity, and
|
|
58
|
+
* persist the account to `~/.dario/accounts/<alias>.json`. Throws — with any
|
|
59
|
+
* upstream secrets redacted — on a failed exchange. Shared by the CLI and the
|
|
60
|
+
* admin API.
|
|
61
|
+
*/
|
|
62
|
+
export declare function completeAddAccount(alias: string, code: string, codeVerifier: string, state: string): Promise<AccountCredentials>;
|
|
42
63
|
export declare function getAccountsDir(): string;
|
|
43
64
|
/**
|
|
44
65
|
* Error subclass for the keychain-import path so the CLI can render
|
package/dist/accounts.js
CHANGED
|
@@ -329,15 +329,11 @@ export async function addAccountViaOAuth(alias) {
|
|
|
329
329
|
* full URL to the browser.
|
|
330
330
|
*/
|
|
331
331
|
export async function addAccountViaManualOAuth(alias) {
|
|
332
|
-
const
|
|
333
|
-
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
334
|
-
// 32-byte state — same constraint as the auto flow. See dario#71.
|
|
335
|
-
const state = base64url(randomBytes(32));
|
|
336
|
-
const authUrl = buildManualAuthorizeUrl(cfg, codeChallenge, state);
|
|
332
|
+
const { authorizeUrl, codeVerifier, state } = await startAddAccount(alias);
|
|
337
333
|
console.log('');
|
|
338
334
|
console.log(` Open this URL in any browser to add account "${alias}":`);
|
|
339
335
|
console.log('');
|
|
340
|
-
console.log(` ${
|
|
336
|
+
console.log(` ${authorizeUrl}`);
|
|
341
337
|
console.log('');
|
|
342
338
|
console.log(' Sign in with the Claude account you want to add. After you approve,');
|
|
343
339
|
console.log(' Anthropic will display an authorization code. Paste it below');
|
|
@@ -351,6 +347,39 @@ export async function addAccountViaManualOAuth(alias) {
|
|
|
351
347
|
if (returnedState && returnedState !== state) {
|
|
352
348
|
throw new Error(`State mismatch — the pasted code is from a different login attempt. Re-run \`dario accounts add ${alias} --manual\` and paste the most recent code.`);
|
|
353
349
|
}
|
|
350
|
+
return completeAddAccount(alias, code, codeVerifier, state);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Non-interactive first half of the manual add-account flow (#599): validate
|
|
354
|
+
* the alias, generate PKCE + state, and build the authorize URL the user opens
|
|
355
|
+
* in a browser. The caller keeps `codeVerifier` + `state` and passes them back
|
|
356
|
+
* to `completeAddAccount` after the user supplies the displayed code. Shared by
|
|
357
|
+
* the `dario accounts add --manual` CLI and the headless admin API — the secret
|
|
358
|
+
* (codeVerifier) never leaves the process that started the flow.
|
|
359
|
+
*/
|
|
360
|
+
export async function startAddAccount(alias) {
|
|
361
|
+
if (!safeAliasPath(alias)) {
|
|
362
|
+
throw new Error(`invalid account alias "${alias}" (allowed: letters, digits, _-. — up to 64 chars, no path separators)`);
|
|
363
|
+
}
|
|
364
|
+
const cfg = await detectCCOAuthConfig();
|
|
365
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
366
|
+
// 32-byte state — same constraint as the auto flow. See dario#71.
|
|
367
|
+
const state = base64url(randomBytes(32));
|
|
368
|
+
const authorizeUrl = buildManualAuthorizeUrl(cfg, codeChallenge, state);
|
|
369
|
+
return { authorizeUrl, codeVerifier, state };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Non-interactive second half (#599): exchange the authorization `code` for
|
|
373
|
+
* tokens (PKCE-verified server-side), attach a device/account identity, and
|
|
374
|
+
* persist the account to `~/.dario/accounts/<alias>.json`. Throws — with any
|
|
375
|
+
* upstream secrets redacted — on a failed exchange. Shared by the CLI and the
|
|
376
|
+
* admin API.
|
|
377
|
+
*/
|
|
378
|
+
export async function completeAddAccount(alias, code, codeVerifier, state) {
|
|
379
|
+
if (!safeAliasPath(alias)) {
|
|
380
|
+
throw new Error(`invalid account alias "${alias}"`);
|
|
381
|
+
}
|
|
382
|
+
const cfg = await detectCCOAuthConfig();
|
|
354
383
|
const tokenRes = await fetch(cfg.tokenUrl, {
|
|
355
384
|
method: 'POST',
|
|
356
385
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless admin API (#599) — an opt-in HTTP control plane for managing the
|
|
3
|
+
* account pool without console access. Mounted at `/admin/*` by the proxy
|
|
4
|
+
* (src/proxy.ts) ONLY when `DARIO_ADMIN=1`.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints (all require the admin bearer token — `DARIO_ADMIN_TOKEN`, or
|
|
7
|
+
* `DARIO_API_KEY` as a fallback — even on loopback, since they add/remove
|
|
8
|
+
* OAuth credentials):
|
|
9
|
+
*
|
|
10
|
+
* POST /admin/login/start { alias } -> { login_id, authorize_url, expires_at }
|
|
11
|
+
* POST /admin/login/complete { login_id, code } -> { alias, status, expires_at }
|
|
12
|
+
* GET /admin/accounts -> { accounts: [...], count }
|
|
13
|
+
* DELETE /admin/accounts/<alias> -> { alias, removed }
|
|
14
|
+
*
|
|
15
|
+
* The login flow mirrors `dario accounts add --manual` (PKCE + manual paste):
|
|
16
|
+
* `/start` returns the authorize URL the operator opens in a browser; they POST
|
|
17
|
+
* the code Anthropic displays back to `/complete`. The PKCE verifier + state
|
|
18
|
+
* live in an in-memory map keyed by `login_id` with a short TTL — never on
|
|
19
|
+
* disk, never returned to the client, single-use.
|
|
20
|
+
*
|
|
21
|
+
* Account changes take effect on the next proxy restart (the pool is built at
|
|
22
|
+
* startup, src/proxy.ts). Hot-reload of a running pool is a deliberate
|
|
23
|
+
* follow-up — keeping this surface small while it manipulates credentials.
|
|
24
|
+
*/
|
|
25
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
26
|
+
export interface AdminDeps {
|
|
27
|
+
/** Admin bearer token buffer; `null` = enabled but no token configured (fail closed). */
|
|
28
|
+
adminTokenBuf: Buffer | null;
|
|
29
|
+
/** Invoked after an account is added or removed (e.g. to log / signal a reload). */
|
|
30
|
+
onAccountsChanged?: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle an `/admin/*` request. Returns `true` if it owned the request (matched
|
|
34
|
+
* one of its routes and wrote a response), `false` if the path isn't one of
|
|
35
|
+
* ours — so the caller's existing routing (incl. the pre-existing
|
|
36
|
+
* `/admin/resume`) and the `DARIO_API_KEY` gate still run.
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleAdminRequest(req: IncomingMessage, res: ServerResponse, urlPath: string, deps: AdminDeps): Promise<boolean>;
|
|
39
|
+
/** Test-only: clear the pending-login map between cases. */
|
|
40
|
+
export declare function _resetAdminStateForTest(): void;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases, loadAccount, } from './accounts.js';
|
|
3
|
+
import { parseManualPaste } from './oauth.js';
|
|
4
|
+
const PENDING_TTL_MS = 10 * 60_000;
|
|
5
|
+
const MAX_PENDING = 64; // backstop against unbounded growth from repeated /start
|
|
6
|
+
const ACCOUNTS_PREFIX = '/admin/accounts/';
|
|
7
|
+
const pendingLogins = new Map();
|
|
8
|
+
function prunePending(now) {
|
|
9
|
+
for (const [id, p] of pendingLogins) {
|
|
10
|
+
if (p.expiresAt <= now)
|
|
11
|
+
pendingLogins.delete(id);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const HEADERS = {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'X-Content-Type-Options': 'nosniff',
|
|
17
|
+
'Cache-Control': 'no-store',
|
|
18
|
+
};
|
|
19
|
+
function send(res, status, body) {
|
|
20
|
+
res.writeHead(status, HEADERS);
|
|
21
|
+
res.end(JSON.stringify(body));
|
|
22
|
+
}
|
|
23
|
+
/** Constant-time bearer / x-api-key check. Fails closed when no token configured. */
|
|
24
|
+
function adminAuthOk(req, tokenBuf) {
|
|
25
|
+
if (!tokenBuf)
|
|
26
|
+
return false;
|
|
27
|
+
const provided = req.headers['x-api-key']
|
|
28
|
+
|| req.headers.authorization?.replace(/^Bearer\s+/i, '');
|
|
29
|
+
if (!provided)
|
|
30
|
+
return false;
|
|
31
|
+
const providedBuf = Buffer.from(provided);
|
|
32
|
+
if (providedBuf.length !== tokenBuf.length)
|
|
33
|
+
return false;
|
|
34
|
+
return timingSafeEqual(providedBuf, tokenBuf);
|
|
35
|
+
}
|
|
36
|
+
async function readJsonBody(req, limitBytes = 64 * 1024) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
req.on('data', (c) => {
|
|
41
|
+
size += c.length;
|
|
42
|
+
if (size > limitBytes) {
|
|
43
|
+
req.destroy();
|
|
44
|
+
reject(new Error('request body too large'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
chunks.push(c);
|
|
48
|
+
});
|
|
49
|
+
req.on('end', () => {
|
|
50
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
51
|
+
if (!raw) {
|
|
52
|
+
resolve({});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
resolve(JSON.parse(raw));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
reject(new Error('invalid JSON body'));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
req.on('error', reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Handle an `/admin/*` request. Returns `true` if it owned the request (matched
|
|
67
|
+
* one of its routes and wrote a response), `false` if the path isn't one of
|
|
68
|
+
* ours — so the caller's existing routing (incl. the pre-existing
|
|
69
|
+
* `/admin/resume`) and the `DARIO_API_KEY` gate still run.
|
|
70
|
+
*/
|
|
71
|
+
export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
72
|
+
const method = req.method ?? 'GET';
|
|
73
|
+
const isAccountDelete = method === 'DELETE' && urlPath.startsWith(ACCOUNTS_PREFIX) && urlPath.length > ACCOUNTS_PREFIX.length;
|
|
74
|
+
const known = urlPath === '/admin/login/start' ||
|
|
75
|
+
urlPath === '/admin/login/complete' ||
|
|
76
|
+
urlPath === '/admin/accounts' ||
|
|
77
|
+
isAccountDelete;
|
|
78
|
+
if (!known)
|
|
79
|
+
return false;
|
|
80
|
+
// Auth — always required, even on loopback (these mutate OAuth credentials).
|
|
81
|
+
if (!adminAuthOk(req, deps.adminTokenBuf)) {
|
|
82
|
+
if (!deps.adminTokenBuf) {
|
|
83
|
+
send(res, 403, { error: 'admin API enabled but no token configured — set DARIO_ADMIN_TOKEN (or DARIO_API_KEY)' });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
send(res, 401, { error: 'Unauthorized', message: 'invalid or missing admin token' });
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
prunePending(now);
|
|
92
|
+
try {
|
|
93
|
+
// POST /admin/login/start { alias }
|
|
94
|
+
if (urlPath === '/admin/login/start') {
|
|
95
|
+
if (method !== 'POST') {
|
|
96
|
+
send(res, 405, { error: 'Method not allowed (use POST)' });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const body = await readJsonBody(req);
|
|
100
|
+
const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
|
|
101
|
+
if (!alias) {
|
|
102
|
+
send(res, 400, { error: 'missing "alias"' });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (pendingLogins.size >= MAX_PENDING) {
|
|
106
|
+
send(res, 429, { error: 'too many pending logins; complete or wait for one to expire' });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const { authorizeUrl, codeVerifier, state } = await startAddAccount(alias); // throws on invalid alias
|
|
110
|
+
const loginId = randomUUID();
|
|
111
|
+
const expiresAt = now + PENDING_TTL_MS;
|
|
112
|
+
pendingLogins.set(loginId, { alias, codeVerifier, state, expiresAt });
|
|
113
|
+
send(res, 200, {
|
|
114
|
+
login_id: loginId,
|
|
115
|
+
authorize_url: authorizeUrl,
|
|
116
|
+
expires_at: new Date(expiresAt).toISOString(),
|
|
117
|
+
instructions: 'Open authorize_url, approve, then POST the displayed code to /admin/login/complete with this login_id.',
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// POST /admin/login/complete { login_id, code }
|
|
122
|
+
if (urlPath === '/admin/login/complete') {
|
|
123
|
+
if (method !== 'POST') {
|
|
124
|
+
send(res, 405, { error: 'Method not allowed (use POST)' });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const body = await readJsonBody(req);
|
|
128
|
+
const loginId = typeof body.login_id === 'string' ? body.login_id : '';
|
|
129
|
+
const rawCode = typeof body.code === 'string' ? body.code : '';
|
|
130
|
+
if (!loginId || !rawCode) {
|
|
131
|
+
send(res, 400, { error: 'missing "login_id" or "code"' });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const p = pendingLogins.get(loginId);
|
|
135
|
+
if (!p || p.expiresAt <= now) {
|
|
136
|
+
pendingLogins.delete(loginId);
|
|
137
|
+
send(res, 410, { error: 'login_id unknown or expired — start a new login' });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
// Accept "code#state" or a bare code; verify the embedded state if present.
|
|
141
|
+
const { code, state: pastedState } = parseManualPaste(rawCode);
|
|
142
|
+
if (!code) {
|
|
143
|
+
send(res, 400, { error: 'no authorization code found in "code"' });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (pastedState && pastedState !== p.state) {
|
|
147
|
+
send(res, 400, { error: 'state mismatch — code is from a different login attempt' });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
pendingLogins.delete(loginId); // single-use, regardless of exchange outcome
|
|
151
|
+
const creds = await completeAddAccount(p.alias, code, p.codeVerifier, p.state);
|
|
152
|
+
deps.onAccountsChanged?.();
|
|
153
|
+
send(res, 200, { alias: creds.alias, status: 'added', expires_at: new Date(creds.expiresAt).toISOString() });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// GET /admin/accounts
|
|
157
|
+
if (urlPath === '/admin/accounts') {
|
|
158
|
+
if (method !== 'GET') {
|
|
159
|
+
send(res, 405, { error: 'Method not allowed (use GET)' });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
const aliases = await listAccountAliases();
|
|
163
|
+
const accounts = (await Promise.all(aliases.map(async (alias) => {
|
|
164
|
+
const a = await loadAccount(alias);
|
|
165
|
+
if (!a)
|
|
166
|
+
return null;
|
|
167
|
+
return { alias: a.alias, scopes: a.scopes, expires_in_ms: Math.max(0, a.expiresAt - now) };
|
|
168
|
+
}))).filter((a) => a !== null);
|
|
169
|
+
send(res, 200, {
|
|
170
|
+
accounts,
|
|
171
|
+
count: accounts.length,
|
|
172
|
+
note: 'live rate-limit / utilization is at GET /accounts when pool mode is active',
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// DELETE /admin/accounts/<alias>
|
|
177
|
+
if (isAccountDelete) {
|
|
178
|
+
const alias = decodeURIComponent(urlPath.slice(ACCOUNTS_PREFIX.length));
|
|
179
|
+
const removed = await removeAccount(alias); // validates alias internally
|
|
180
|
+
if (removed)
|
|
181
|
+
deps.onAccountsChanged?.();
|
|
182
|
+
send(res, removed ? 200 : 404, { alias, removed });
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
send(res, 405, { error: 'Method not allowed' });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
// startAddAccount throws on an invalid alias; completeAddAccount throws
|
|
190
|
+
// (with secrets redacted) on a failed token exchange; readJsonBody throws
|
|
191
|
+
// on oversized / malformed bodies.
|
|
192
|
+
send(res, 400, { error: err.message });
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Test-only: clear the pending-login map between cases. */
|
|
197
|
+
export function _resetAdminStateForTest() {
|
|
198
|
+
pendingLogins.clear();
|
|
199
|
+
}
|
package/dist/proxy.js
CHANGED
|
@@ -16,6 +16,7 @@ import { Analytics, billingBucketFromClaim } from './analytics.js';
|
|
|
16
16
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
17
17
|
import { notify as osNotify } from './notify.js';
|
|
18
18
|
import { loadAllAccounts, loadAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale } from './accounts.js';
|
|
19
|
+
import { handleAdminRequest } from './admin-api.js';
|
|
19
20
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
20
21
|
import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
|
|
21
22
|
import { redactSecrets } from './redact.js';
|
|
@@ -1115,6 +1116,17 @@ export async function startProxy(opts = {}) {
|
|
|
1115
1116
|
// Optional proxy authentication — pre-encode key buffer for performance
|
|
1116
1117
|
const apiKey = process.env.DARIO_API_KEY;
|
|
1117
1118
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
1119
|
+
// Admin API (#599) — opt-in headless account management at /admin/*. Off
|
|
1120
|
+
// unless DARIO_ADMIN=1. Auth is ALWAYS required (even on loopback) because
|
|
1121
|
+
// these endpoints add/remove OAuth accounts: the admin token is
|
|
1122
|
+
// DARIO_ADMIN_TOKEN, falling back to DARIO_API_KEY. Enabled-but-tokenless
|
|
1123
|
+
// fails closed (403) so account control is never left open on loopback.
|
|
1124
|
+
const adminEnabled = process.env.DARIO_ADMIN === '1';
|
|
1125
|
+
const adminToken = process.env.DARIO_ADMIN_TOKEN || process.env.DARIO_API_KEY || '';
|
|
1126
|
+
const adminTokenBuf = adminToken ? Buffer.from(adminToken) : null;
|
|
1127
|
+
if (adminEnabled) {
|
|
1128
|
+
console.log(`[dario] admin API enabled at /admin/* (token ${adminTokenBuf ? 'configured' : 'MISSING — endpoints return 403 until DARIO_ADMIN_TOKEN is set'})`);
|
|
1129
|
+
}
|
|
1118
1130
|
// CORS origin defaults to the localhost URL the proxy is served at. Users
|
|
1119
1131
|
// binding to a non-loopback address (e.g. a Tailscale interface) can
|
|
1120
1132
|
// override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
|
|
@@ -1179,6 +1191,23 @@ export async function startProxy(opts = {}) {
|
|
|
1179
1191
|
res.end(JSON.stringify(body));
|
|
1180
1192
|
return;
|
|
1181
1193
|
}
|
|
1194
|
+
// Admin API (#599) — self-authenticating with the admin token, mounted
|
|
1195
|
+
// BEFORE the DARIO_API_KEY gate so it requires its own token even on
|
|
1196
|
+
// loopback (where the proxy key is otherwise optional). handleAdminRequest
|
|
1197
|
+
// only acts on its own routes (/admin/login/*, /admin/accounts) and returns
|
|
1198
|
+
// false otherwise, so the pre-existing /admin/resume + the key gate below
|
|
1199
|
+
// still apply unchanged.
|
|
1200
|
+
if (adminEnabled && urlPath.startsWith('/admin/')) {
|
|
1201
|
+
const handled = await handleAdminRequest(req, res, urlPath, {
|
|
1202
|
+
adminTokenBuf,
|
|
1203
|
+
onAccountsChanged: () => {
|
|
1204
|
+
if (verbose)
|
|
1205
|
+
console.log('[dario] admin: account pool changed on disk (effective on next proxy restart)');
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
if (handled)
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1182
1211
|
if (!checkAuth(req)) {
|
|
1183
1212
|
if (verbose) {
|
|
1184
1213
|
// Silent auth rejects are hard to diagnose when a client's config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.106",
|
|
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": {
|