@askalf/dario 6.7.1 → 6.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/admin-api.d.ts +11 -1
- package/dist/admin-api.js +101 -2
- package/dist/cli.js +213 -3
- package/dist/keys.d.ts +127 -0
- package/dist/keys.js +346 -0
- package/dist/ledger.d.ts +34 -1
- package/dist/ledger.js +122 -9
- package/dist/proxy.d.ts +11 -0
- package/dist/proxy.js +88 -10
- package/docs/admin-api.md +21 -10
- package/docs/api-equivalent-spend.md +16 -1
- package/docs/commands.md +3 -1
- package/docs/configuration.md +7 -0
- package/docs/keys.md +112 -0
- package/docs/multi-account-pool.md +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
<p><strong>One local endpoint. Every AI tool you own. The subscriptions you already pay for.</strong></p>
|
|
29
29
|
|
|
30
|
-
<sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~
|
|
30
|
+
<sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~38k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
|
|
31
31
|
|
|
32
32
|
<sub><a href="#start-in-60-seconds">Start</a> · <a href="#point-your-tools-at-it">Your tools</a> · <a href="#what-it-does-with-a-request">Routing</a> · <a href="#two-plans-one-endpoint">Two plans</a> · <a href="#many-seats-one-endpoint">Pool</a> · <a href="#it-tracks-a-moving-target">Drift</a> · <a href="#trust--transparency">Trust</a> · <a href="#will-my-account-get-suspended">Risk</a> · <a href="#commands">Commands</a> · <a href="#faq">FAQ</a> · <a href="docs/returning.md">Coming back after a while?</a></sub>
|
|
33
33
|
|
|
@@ -373,6 +373,17 @@ Three things it does that a round-robin doesn't:
|
|
|
373
373
|
|
|
374
374
|
`--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups. Refresh tokens expire about 28 days after the original grant regardless of rotation, so every seat's grant age is tracked and surfaced in `dario accounts list`, `dario doctor` and `GET /accounts` before it becomes a silent outage. Provision over HTTP with the headless [admin API](./docs/admin-api.md); pin one request to one seat with `dario accounts check <alias>` (admin API required: `DARIO_ADMIN=1` and a `DARIO_ADMIN_TOKEN`). Internals and the live `/accounts` + `/analytics` endpoints: [multi-account-pool.md](./docs/multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](./test/pool-e2e.mjs).
|
|
375
375
|
|
|
376
|
+
### One key per developer
|
|
377
|
+
|
|
378
|
+
A shared dario serves several people through one `DARIO_API_KEY`, and nothing says whose traffic is whose except a header any client can set. Since 6.8 a **named key** ties attribution to the credential:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
dario keys create alice
|
|
382
|
+
dario keys create bob --seat=bobs-max --models=claude-sonnet-5,claude-haiku*
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The secret is printed once and only its hash is kept, in `~/.dario/keys.json`. The request authenticated with alice's key *is* alice's in `/analytics`, in the ledger (`dario usage --by-key`) and on every log line; a key can prefer one pool seat (taken while it has headroom, normal routing otherwise, so a developer's conversations ride their own subscription) and can be held to a model allowlist (`403` before anything goes upstream, in either wire shape). The running proxy picks up a created, rotated or revoked key on its next request; the root `DARIO_API_KEY` keeps working beside them; `/admin/keys` does the same over HTTP. Details: [keys.md](./docs/keys.md).
|
|
386
|
+
|
|
376
387
|
### Watch it happen
|
|
377
388
|
|
|
378
389
|
Type `dario` with no arguments for a full-screen control panel: live request stream, per-model burn rate, rate-limit utilization per seat, billing-bucket breakdown, and an in-place config editor that writes `~/.dario/config.json`. Pure ANSI, zero new runtime deps. <kbd>Tab</kbd> moves between tabs, <kbd>r</kbd> refreshes, <kbd>R</kbd> resumes a halted overage guard, <kbd>q</kbd> quits.
|
|
@@ -515,9 +526,10 @@ Longer version, with specifics: [#68](https://github.com/askalf/dario/discussion
|
|
|
515
526
|
| `dario doctor [--usage] [--probe] [--obedience] [--auth-check] [--bun-bootstrap] [--json]` | One aggregated health report: runtime/TLS, template and drift, OAuth, pool, refresh-grant age, failover readiness, backends |
|
|
516
527
|
| `dario add altman` / `dario add amodei` | Attach a ChatGPT plan / a Claude account, by whose it is |
|
|
517
528
|
| `dario accounts list` / `add` / `remove` / `check <alias>` | Pool management; `check` sends one pinned request per model through the running proxy (admin API on) |
|
|
529
|
+
| `dario keys create <name>` / `list` / `revoke` / `rotate` | One credential per developer on a shared dario: attributed by key, optional preferred seat and model allowlist, hashes on disk ([keys.md](./docs/keys.md)) |
|
|
518
530
|
| `dario backend list` / `add` / `remove` | OpenAI-compatible API-key backends |
|
|
519
531
|
| `dario codex list` / `add` / `remove` | ChatGPT accounts (the long form of `dario add altman`) |
|
|
520
|
-
| `dario usage` · `dario compare` · `dario config` · `dario status` | Lifetime API-equivalent spend + burn rate for the last hour (`--card` writes the share card) · read the shadow-compare log · effective config, redacted · token health |
|
|
532
|
+
| `dario usage` · `dario compare` · `dario config` · `dario status` | Lifetime API-equivalent spend + burn rate for the last hour (`--card` writes the share card, `--by-key` splits it per key) · read the shadow-compare log · effective config, redacted · token health |
|
|
521
533
|
| `dario resume` · `dario refresh` · `dario logout` · `dario upgrade` | Clear an overage halt · force a token refresh · delete credentials · safe self-update |
|
|
522
534
|
| `dario mcp` · `dario subagent install` / `remove` / `status` | Reach dario from inside any MCP client, or from inside a Claude Code session, read-only |
|
|
523
535
|
|
|
@@ -529,7 +541,7 @@ Longer version, with specifics: [#68](https://github.com/askalf/dario/discussion
|
|
|
529
541
|
| `GET /status` · `GET /accounts` · `GET /analytics` | OAuth detail · per-seat utilization and grant age · per-account / per-model stats and burn rate |
|
|
530
542
|
| `POST /v1/messages/count_tokens` · `POST /v1/complete` | Token counting and the legacy Text Completions shape |
|
|
531
543
|
| `GET /analytics/stream` · `GET /analytics/ledger` · `GET /codex` | Live analytics over SSE · the ledger's per-day table · ChatGPT-seat status, read without spending or exposing a token |
|
|
532
|
-
| `/admin/*` | Provisioning, `GET /admin/accounts`, `POST /admin/resume`; only with `DARIO_ADMIN=1` ([admin API](./docs/admin-api.md)) |
|
|
544
|
+
| `/admin/*` | Provisioning, `GET /admin/accounts`, `/admin/keys`, `POST /admin/resume`; only with `DARIO_ADMIN=1` ([admin API](./docs/admin-api.md)) |
|
|
533
545
|
|
|
534
546
|
Flags: [commands.md](./docs/commands.md), plus `dario --help` for the ones it doesn't list yet (`--effort`, `--max-tokens`, `--model-alias`, `--fast-model`, session rotation, concurrency caps, the pacing knobs behind `--stealth`) · env vars grouped by task, for Docker / k8s / systemd: [configuration.md](./docs/configuration.md) · SDK examples: [usage.md](./docs/usage.md).
|
|
535
547
|
|
package/dist/admin-api.d.ts
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
* against it.
|
|
69
69
|
*/
|
|
70
70
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
71
|
+
import { type KeyStore } from './keys.js';
|
|
71
72
|
/** Persisted account metadata surfaced by `GET /admin/accounts`. */
|
|
72
73
|
export interface AdminAccountRecord {
|
|
73
74
|
alias: string;
|
|
@@ -142,11 +143,13 @@ export interface AdminAccountLive {
|
|
|
142
143
|
}
|
|
143
144
|
/** An audited admin action — see `AdminDeps.audit`. Never carries secrets. */
|
|
144
145
|
export interface AdminAuditEvent {
|
|
145
|
-
action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited';
|
|
146
|
+
action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited' | 'key_create' | 'key_revoke' | 'key_rotate';
|
|
146
147
|
ok: boolean;
|
|
147
148
|
status: number;
|
|
148
149
|
/** Account alias, when the action targets one. */
|
|
149
150
|
alias?: string;
|
|
151
|
+
/** Named key, when the action targets one (dario#1318). The name only, never the secret. */
|
|
152
|
+
key?: string;
|
|
150
153
|
/** Client address (`req.socket.remoteAddress`), when known. */
|
|
151
154
|
remote?: string;
|
|
152
155
|
/** Extra context, e.g. the rate-limited category ('auth' | 'mutation'). */
|
|
@@ -187,6 +190,13 @@ export interface AdminDeps {
|
|
|
187
190
|
* auth are never gated. Absent = no limiting. Owned by the proxy (#620).
|
|
188
191
|
*/
|
|
189
192
|
rateLimit?: (category: 'auth' | 'mutation') => number;
|
|
193
|
+
/**
|
|
194
|
+
* Named keys (dario#1318): the store the running proxy authenticates
|
|
195
|
+
* from, so `/admin/keys` edits the same file `dario keys` does and a key
|
|
196
|
+
* minted here works on the next request. `null` / absent = named keys are
|
|
197
|
+
* off on this proxy; the routes answer 404.
|
|
198
|
+
*/
|
|
199
|
+
keys?: KeyStore | null;
|
|
190
200
|
}
|
|
191
201
|
/**
|
|
192
202
|
* Handle an `/admin/*` request. Returns `true` if it owned the request (matched
|
package/dist/admin-api.js
CHANGED
|
@@ -3,9 +3,11 @@ import { timingSafeEqual } from 'node:crypto';
|
|
|
3
3
|
import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases, loadAccount, } from './accounts.js';
|
|
4
4
|
import { parseManualPaste } from './oauth.js';
|
|
5
5
|
import { grantAge } from './refresh-grant.js';
|
|
6
|
+
import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, KEY_NAME_RE } from './keys.js';
|
|
6
7
|
const PENDING_TTL_MS = 10 * 60_000;
|
|
7
8
|
const MAX_PENDING = 64; // backstop against unbounded growth (distinct aliases)
|
|
8
9
|
const ACCOUNTS_PREFIX = '/admin/accounts/';
|
|
10
|
+
const KEYS_PREFIX = '/admin/keys/';
|
|
9
11
|
/**
|
|
10
12
|
* `consecutiveAuthFailures` floor for `/admin/login/start-needed` to treat an
|
|
11
13
|
* account as needing a new login rather than mid-blip. Empirically: 1 failure
|
|
@@ -175,11 +177,23 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
175
177
|
const method = req.method ?? 'GET';
|
|
176
178
|
const remote = req.socket?.remoteAddress;
|
|
177
179
|
const isAccountDelete = method === 'DELETE' && urlPath.startsWith(ACCOUNTS_PREFIX) && urlPath.length > ACCOUNTS_PREFIX.length;
|
|
180
|
+
// Named keys (dario#1318): `/admin/keys`, `/admin/keys/<name>`,
|
|
181
|
+
// `/admin/keys/<name>/rotate`. The name is validated after auth so a
|
|
182
|
+
// malformed one is a 400 to a caller who holds the token, not a route miss.
|
|
183
|
+
const keyTarget = urlPath.startsWith(KEYS_PREFIX) && urlPath.length > KEYS_PREFIX.length
|
|
184
|
+
? decodeURIComponent(urlPath.slice(KEYS_PREFIX.length))
|
|
185
|
+
: null;
|
|
186
|
+
const isKeyRotate = keyTarget !== null && keyTarget.endsWith('/rotate');
|
|
187
|
+
const keyName = keyTarget === null ? null : isKeyRotate ? keyTarget.slice(0, -'/rotate'.length) : keyTarget;
|
|
188
|
+
const isKeyRevoke = keyTarget !== null && !isKeyRotate && method === 'DELETE';
|
|
178
189
|
const known = urlPath === '/admin/login/start' ||
|
|
179
190
|
urlPath === '/admin/login/start-needed' ||
|
|
180
191
|
urlPath === '/admin/login/complete' ||
|
|
181
192
|
urlPath === '/admin/accounts' ||
|
|
182
|
-
|
|
193
|
+
urlPath === '/admin/keys' ||
|
|
194
|
+
isAccountDelete ||
|
|
195
|
+
isKeyRotate ||
|
|
196
|
+
isKeyRevoke;
|
|
183
197
|
if (!known)
|
|
184
198
|
return false;
|
|
185
199
|
// Auth — always required, even on loopback (these mutate OAuth credentials).
|
|
@@ -209,7 +223,8 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
209
223
|
// below). That is a deliberate cost-accounting choice, not an oversight: a
|
|
210
224
|
// blanket pre-parse token here would let a single HTTP request move N
|
|
211
225
|
// accounts' credentials for the price of one throttle token.
|
|
212
|
-
const isMutation = urlPath === '/admin/login/start' || isAccountDelete
|
|
226
|
+
const isMutation = urlPath === '/admin/login/start' || isAccountDelete
|
|
227
|
+
|| (urlPath === '/admin/keys' && method === 'POST') || isKeyRotate || isKeyRevoke;
|
|
213
228
|
if (isMutation) {
|
|
214
229
|
const wait = deps.rateLimit?.('mutation') ?? 0;
|
|
215
230
|
if (wait > 0) {
|
|
@@ -415,6 +430,90 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
415
430
|
send(res, removed ? 200 : 404, { alias, removed });
|
|
416
431
|
return true;
|
|
417
432
|
}
|
|
433
|
+
// Named keys (dario#1318). Every route below edits the store the proxy
|
|
434
|
+
// authenticates from; the secret appears in exactly one response and is
|
|
435
|
+
// never stored, listed or logged.
|
|
436
|
+
if (urlPath === '/admin/keys' || keyName !== null) {
|
|
437
|
+
const store = deps.keys ?? null;
|
|
438
|
+
if (!store) {
|
|
439
|
+
send(res, 404, { error: 'named keys are off on this proxy', hint: 'start without --no-keys / DARIO_KEYS=0' });
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
if (store.error) {
|
|
443
|
+
send(res, 503, { error: `keys file unreadable: ${store.error}`, path: store.path });
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
// GET /admin/keys — every key, hashes excluded.
|
|
447
|
+
if (urlPath === '/admin/keys' && method === 'GET') {
|
|
448
|
+
const keys = store.list(now);
|
|
449
|
+
send(res, 200, { keys, count: keys.length, path: store.path });
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
// POST /admin/keys { name, seat?, models?, expires? } — the secret, once.
|
|
453
|
+
if (urlPath === '/admin/keys' && method === 'POST') {
|
|
454
|
+
const body = await readJsonBody(req);
|
|
455
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
456
|
+
if (!KEY_NAME_RE.test(name)) {
|
|
457
|
+
send(res, 400, { error: 'invalid or missing "name": letters, digits, _ - . only, up to 64, starting with a letter or digit' });
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
const seat = typeof body.seat === 'string' && body.seat.trim() ? body.seat.trim() : undefined;
|
|
461
|
+
const models = Array.isArray(body.models)
|
|
462
|
+
? body.models.filter((m) => typeof m === 'string' && m.trim().length > 0).map((m) => m.trim())
|
|
463
|
+
: typeof body.models === 'string' ? body.models.split(',').map((m) => m.trim()).filter(Boolean) : undefined;
|
|
464
|
+
let expiresAt;
|
|
465
|
+
if (typeof body.expires === 'string' && body.expires.trim()) {
|
|
466
|
+
const parsed = parseExpiry(body.expires, now);
|
|
467
|
+
if (parsed === null) {
|
|
468
|
+
send(res, 400, { error: 'invalid "expires": use 30d, 12h, 2w, or an ISO date' });
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
expiresAt = parsed;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, now }));
|
|
475
|
+
deps.audit?.({ action: 'key_create', ok: true, status: 201, key: made.record.name, remote, detail: seat ? `seat=${seat}` : undefined });
|
|
476
|
+
send(res, 201, { key: publicKey(made.record, now), secret: made.secret, note: 'the secret is shown once and is not stored' });
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
const message = err.message;
|
|
480
|
+
const status = /already exists/.test(message) ? 409 : 400;
|
|
481
|
+
deps.audit?.({ action: 'key_create', ok: false, status, key: name, remote });
|
|
482
|
+
send(res, status, { error: message });
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (urlPath === '/admin/keys') {
|
|
487
|
+
send(res, 405, { error: 'Method not allowed (use GET or POST)' });
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
if (!KEY_NAME_RE.test(keyName)) {
|
|
491
|
+
send(res, 400, { error: 'invalid key name' });
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
// POST /admin/keys/<name>/rotate — a new secret under the same name; the old one stops at once.
|
|
495
|
+
if (isKeyRotate) {
|
|
496
|
+
if (method !== 'POST') {
|
|
497
|
+
send(res, 405, { error: 'Method not allowed (use POST)' });
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
const rotated = store.mutate((file) => rotateKey(file, keyName, now));
|
|
501
|
+
deps.audit?.({ action: 'key_rotate', ok: rotated !== null, status: rotated ? 200 : 404, key: keyName, remote });
|
|
502
|
+
if (!rotated) {
|
|
503
|
+
send(res, 404, { error: `no key named "${keyName}"` });
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
send(res, 200, { key: publicKey(rotated.record, now), secret: rotated.secret, note: 'the secret is shown once and is not stored' });
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
// DELETE /admin/keys/<name> — revoked, kept for the list.
|
|
510
|
+
if (isKeyRevoke) {
|
|
511
|
+
const revoked = store.mutate((file) => revokeKey(file, keyName));
|
|
512
|
+
deps.audit?.({ action: 'key_revoke', ok: revoked, status: revoked ? 200 : 404, key: keyName, remote });
|
|
513
|
+
send(res, revoked ? 200 : 404, { name: keyName, revoked });
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
418
517
|
send(res, 405, { error: 'Method not allowed' });
|
|
419
518
|
return true;
|
|
420
519
|
}
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
|
|
18
18
|
// other startup side effect.
|
|
19
19
|
import { unlink, writeFile } from 'node:fs/promises';
|
|
20
|
-
import { formatLedgerSummary, formatUsd, renderLedgerCard, readLedgerFile, resolveLedgerPath, summarizeLedger } from './ledger.js';
|
|
20
|
+
import { formatLedgerSummary, formatLedgerConsumers, formatUsd, renderLedgerCard, readLedgerFile, resolveLedgerPath, summarizeLedger } from './ledger.js';
|
|
21
|
+
import { KeyStore, createKey, revokeKey, rotateKey, deleteKey, parseExpiry, publicKey, resolveKeysPath, KEY_NAME_RE } from './keys.js';
|
|
21
22
|
import { loadAllAccounts as loadAllAccountsForIdentity, regenerateClientIdentity } from './accounts.js';
|
|
22
23
|
import { maskEmail } from './pool.js';
|
|
23
24
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
@@ -635,6 +636,12 @@ async function proxy() {
|
|
|
635
636
|
// On by default; see ProxyOptions.ledger.
|
|
636
637
|
const ledger = !(args.includes('--no-ledger')
|
|
637
638
|
|| ['0', 'false', 'no', 'off'].includes((process.env['DARIO_LEDGER'] ?? '').toLowerCase()));
|
|
639
|
+
// --no-keys / DARIO_KEYS=0 — ignore ~/.dario/keys.json (v6.8, dario#1318);
|
|
640
|
+
// --keys-path=<file> / DARIO_KEYS_PATH moves it. See ProxyOptions.keys.
|
|
641
|
+
const keys = !(args.includes('--no-keys')
|
|
642
|
+
|| ['0', 'false', 'no', 'off'].includes((process.env['DARIO_KEYS'] ?? '').toLowerCase()));
|
|
643
|
+
const keysPathArg = args.find(a => a.startsWith('--keys-path='));
|
|
644
|
+
const keysPath = keysPathArg ? keysPathArg.slice('--keys-path='.length) : undefined;
|
|
638
645
|
// --preserve-output-format — carry the client body's `output_config.format`
|
|
639
646
|
// (structured-output JSON schema) through to upstream instead of dropping it
|
|
640
647
|
// during the CC rebuild. See ProxyOptions.preserveOutputFormat for rationale.
|
|
@@ -659,7 +666,166 @@ async function proxy() {
|
|
|
659
666
|
console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
|
|
660
667
|
process.exit(1);
|
|
661
668
|
}
|
|
662
|
-
await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, poolStrategy, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger });
|
|
669
|
+
await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, poolStrategy, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger, keys, keysPath });
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* `dario keys` — named keys for a shared dario (v6.8, dario#1318). One
|
|
673
|
+
* credential per developer, stored as a hash in ~/.dario/keys.json; the
|
|
674
|
+
* secret is printed once, here, and nowhere else. The running proxy re-reads
|
|
675
|
+
* the file on the next request, so nothing restarts.
|
|
676
|
+
*/
|
|
677
|
+
async function keys() {
|
|
678
|
+
const sub = args[1];
|
|
679
|
+
const asJson = args.includes('--json');
|
|
680
|
+
const pathArg = args.find(a => a.startsWith('--keys-path='));
|
|
681
|
+
const path = pathArg ? pathArg.slice('--keys-path='.length) : resolveKeysPath();
|
|
682
|
+
const store = new KeyStore(path);
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
const fmtDay = (iso) => iso ? iso.slice(0, 10) : '-';
|
|
685
|
+
const fmtAgo = (iso) => {
|
|
686
|
+
if (!iso)
|
|
687
|
+
return 'never';
|
|
688
|
+
const ms = now - Date.parse(iso);
|
|
689
|
+
if (ms < 60_000)
|
|
690
|
+
return 'just now';
|
|
691
|
+
if (ms < 3_600_000)
|
|
692
|
+
return `${Math.floor(ms / 60_000)}m ago`;
|
|
693
|
+
if (ms < 86_400_000)
|
|
694
|
+
return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
695
|
+
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
696
|
+
};
|
|
697
|
+
const printSecret = (verb, k, secret) => {
|
|
698
|
+
if (asJson) {
|
|
699
|
+
process.stdout.write(JSON.stringify({ key: k, secret }, null, 2) + '\n');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
console.log('');
|
|
703
|
+
console.log(` Key "${k.name}" ${verb} (id ${k.id}).`);
|
|
704
|
+
console.log('');
|
|
705
|
+
console.log(` ${secret}`);
|
|
706
|
+
console.log('');
|
|
707
|
+
console.log(' Shown once; dario keeps only a hash. Give it to the client as its API');
|
|
708
|
+
console.log(' key — every request it authenticates is attributed to this name in');
|
|
709
|
+
console.log(' /analytics, the ledger (`dario usage --by-key`) and the log.');
|
|
710
|
+
if (k.seat)
|
|
711
|
+
console.log(` Preferred seat: ${k.seat} (used when it has headroom; otherwise normal routing)`);
|
|
712
|
+
if (k.models.length > 0)
|
|
713
|
+
console.log(` Models: ${k.models.join(', ')} (anything else is refused with 403)`);
|
|
714
|
+
if (k.expires)
|
|
715
|
+
console.log(` Expires: ${k.expires}`);
|
|
716
|
+
console.log(' A running proxy picks this up on its next request; no restart.');
|
|
717
|
+
console.log('');
|
|
718
|
+
};
|
|
719
|
+
if (!sub || sub === 'list' || sub === 'ls') {
|
|
720
|
+
store.load();
|
|
721
|
+
if (store.error) {
|
|
722
|
+
console.error(`[dario] keys: ${path} is unreadable: ${store.error}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
const list = store.list(now);
|
|
726
|
+
if (asJson) {
|
|
727
|
+
process.stdout.write(JSON.stringify({ path, keys: list, count: list.length }, null, 2) + '\n');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
console.log('');
|
|
731
|
+
console.log(' dario — Keys');
|
|
732
|
+
console.log(' ────────────');
|
|
733
|
+
console.log('');
|
|
734
|
+
if (list.length === 0) {
|
|
735
|
+
console.log(` No named keys yet (${path}).`);
|
|
736
|
+
console.log('');
|
|
737
|
+
console.log(' One key per developer on a shared dario, attributed by credential:');
|
|
738
|
+
console.log('');
|
|
739
|
+
console.log(' dario keys create alice');
|
|
740
|
+
console.log(' dario keys create bob --seat=bobs-max --models=claude-sonnet-5');
|
|
741
|
+
console.log('');
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const w = Math.max(4, ...list.map((k) => k.name.length));
|
|
745
|
+
console.log(` ${'NAME'.padEnd(w)} ${'STATUS'.padEnd(7)} ${'SEAT'.padEnd(12)} ${'LAST USED'.padEnd(10)} ${'EXPIRES'.padEnd(10)} MODELS`);
|
|
746
|
+
for (const k of list) {
|
|
747
|
+
console.log(` ${k.name.padEnd(w)} ${k.status.padEnd(7)} ${(k.seat ?? '-').padEnd(12)} ${fmtAgo(k.last_used).padEnd(10)} ${fmtDay(k.expires).padEnd(10)} ${k.models.length ? k.models.join(', ') : 'any'}`);
|
|
748
|
+
}
|
|
749
|
+
console.log('');
|
|
750
|
+
console.log(` ${list.length} key${list.length === 1 ? '' : 's'} in ${path}. Spend per key: dario usage --by-key`);
|
|
751
|
+
console.log('');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (sub === 'create' || sub === 'add' || sub === 'new') {
|
|
755
|
+
const name = args[2];
|
|
756
|
+
if (!name || name.startsWith('--')) {
|
|
757
|
+
console.error('');
|
|
758
|
+
console.error(' Usage: dario keys create <name> [--seat=<alias>] [--models=a,b,prefix*] [--expires=30d|12h|2w|<ISO date>]');
|
|
759
|
+
console.error('');
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
const seatArg = args.find(a => a.startsWith('--seat='));
|
|
763
|
+
const modelsArg = args.find(a => a.startsWith('--models='));
|
|
764
|
+
const expiresArg = args.find(a => a.startsWith('--expires='));
|
|
765
|
+
const seat = seatArg ? seatArg.slice('--seat='.length).trim() : undefined;
|
|
766
|
+
const models = modelsArg ? modelsArg.slice('--models='.length).split(',').map((m) => m.trim()).filter(Boolean) : undefined;
|
|
767
|
+
let expiresAt;
|
|
768
|
+
if (expiresArg) {
|
|
769
|
+
const parsed = parseExpiry(expiresArg.slice('--expires='.length), now);
|
|
770
|
+
if (parsed === null) {
|
|
771
|
+
console.error(`[dario] --expires: "${expiresArg.slice('--expires='.length)}" is not 30d, 12h, 2w or an ISO date.`);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
expiresAt = parsed;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, now }));
|
|
778
|
+
printSecret('created', publicKey(made.record, now), made.secret);
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
console.error(`[dario] ${err instanceof Error ? err.message : String(err)}`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (sub === 'rotate' || sub === 'revoke' || sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
787
|
+
const name = args[2];
|
|
788
|
+
if (!name || !KEY_NAME_RE.test(name)) {
|
|
789
|
+
console.error('');
|
|
790
|
+
console.error(` Usage: dario keys ${sub} <name>`);
|
|
791
|
+
console.error('');
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
if (sub === 'rotate') {
|
|
796
|
+
const rotated = store.mutate((file) => rotateKey(file, name, now));
|
|
797
|
+
if (!rotated) {
|
|
798
|
+
console.error(`[dario] No key named "${name}".`);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
printSecret('rotated — the old secret stopped working', publicKey(rotated.record, now), rotated.secret);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (sub === 'revoke') {
|
|
805
|
+
const ok = store.mutate((file) => revokeKey(file, name));
|
|
806
|
+
if (!ok) {
|
|
807
|
+
console.error(`[dario] No key named "${name}".`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
console.log(`[dario] Key "${name}" revoked. It stays in the list; \`dario keys remove ${name}\` forgets it.`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const ok = store.mutate((file) => deleteKey(file, name));
|
|
814
|
+
if (!ok) {
|
|
815
|
+
console.error(`[dario] No key named "${name}".`);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
console.log(`[dario] Key "${name}" removed.`);
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
console.error(`[dario] ${err instanceof Error ? err.message : String(err)}`);
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
console.error(`[dario] Unknown keys subcommand: ${sub}`);
|
|
827
|
+
console.error('Usage: dario keys [list|create <name> [--seat=..] [--models=..] [--expires=..]|revoke <name>|rotate <name>|remove <name>] [--json] [--keys-path=<file>]');
|
|
828
|
+
process.exit(1);
|
|
663
829
|
}
|
|
664
830
|
/**
|
|
665
831
|
* Parse `--system-prompt=<verbatim|partial|aggressive|filepath>` (or the
|
|
@@ -1505,6 +1671,25 @@ async function help() {
|
|
|
1505
1671
|
entry by its platform identifier (Linux account
|
|
1506
1672
|
attribute, Windows TargetName).
|
|
1507
1673
|
dario accounts remove N Remove an account from the pool
|
|
1674
|
+
dario keys create NAME [--seat=ALIAS] [--models=a,b,prefix*] [--expires=30d]
|
|
1675
|
+
Mint a named key for one developer on a shared
|
|
1676
|
+
dario (v6.8). Prints the secret once; stores a
|
|
1677
|
+
hash in ~/.dario/keys.json. Requests it
|
|
1678
|
+
authenticates are attributed to NAME in
|
|
1679
|
+
/analytics, the ledger and the log, next to
|
|
1680
|
+
the root DARIO_API_KEY which keeps working.
|
|
1681
|
+
--seat prefers a pool seat when it has
|
|
1682
|
+
headroom (normal routing otherwise); --models
|
|
1683
|
+
refuses any other model with 403 before
|
|
1684
|
+
anything goes upstream; --expires refuses the
|
|
1685
|
+
key after 30d / 12h / 2w / an ISO date. The
|
|
1686
|
+
running proxy sees it on the next request.
|
|
1687
|
+
dario keys list Named keys: status, seat, last used, expiry,
|
|
1688
|
+
models. --json for the raw list.
|
|
1689
|
+
dario keys revoke NAME Refuse a key from now on (kept in the list).
|
|
1690
|
+
dario keys rotate NAME New secret, same name and settings; the old
|
|
1691
|
+
secret stops at once.
|
|
1692
|
+
dario keys remove NAME Forget a key entirely.
|
|
1508
1693
|
dario codex list List ChatGPT-subscription accounts, served on
|
|
1509
1694
|
/v1/chat/completions.
|
|
1510
1695
|
dario codex add NAME Add a ChatGPT-subscription account (prints an
|
|
@@ -1594,6 +1779,9 @@ async function help() {
|
|
|
1594
1779
|
down). --card[=file.svg] writes a share
|
|
1595
1780
|
card of that number (default
|
|
1596
1781
|
dario-api-equivalent.svg). (v6.6)
|
|
1782
|
+
--by-key splits the lifetime number per
|
|
1783
|
+
consumer: named key, x-dario-consumer
|
|
1784
|
+
header, or hashed user id. (v6.8)
|
|
1597
1785
|
dario compare Read the shadow-compare log written by
|
|
1598
1786
|
requests carrying \`x-dario-compare\`:
|
|
1599
1787
|
per-model calls, success rate, median
|
|
@@ -1729,6 +1917,10 @@ async function help() {
|
|
|
1729
1917
|
first request, across restarts. Env:
|
|
1730
1918
|
DARIO_LEDGER=0; DARIO_LEDGER_PATH=<file>
|
|
1731
1919
|
moves it. (v6.6)
|
|
1920
|
+
--no-keys Ignore named keys (~/.dario/keys.json): only
|
|
1921
|
+
DARIO_API_KEY authenticates. Env: DARIO_KEYS=0;
|
|
1922
|
+
--keys-path=<file> / DARIO_KEYS_PATH moves the
|
|
1923
|
+
file. See \`dario keys\`. (v6.8)
|
|
1732
1924
|
--session-idle-rotate=MS Idle ms before an account's session id
|
|
1733
1925
|
rotates (default: 900000 = 15 min).
|
|
1734
1926
|
Real CC rotates once per conversation, not
|
|
@@ -2353,8 +2545,13 @@ async function usage() {
|
|
|
2353
2545
|
const url = `http://127.0.0.1:${port}/analytics`;
|
|
2354
2546
|
let payload = null;
|
|
2355
2547
|
let connectError = null;
|
|
2548
|
+
// A proxy with DARIO_API_KEY set gates /analytics too; present the key when
|
|
2549
|
+
// the environment has it, as `accounts list --live` does (v6.8).
|
|
2550
|
+
const usageHeaders = {};
|
|
2551
|
+
if (process.env['DARIO_API_KEY'])
|
|
2552
|
+
usageHeaders['x-api-key'] = process.env['DARIO_API_KEY'];
|
|
2356
2553
|
try {
|
|
2357
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
2554
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000), headers: usageHeaders });
|
|
2358
2555
|
if (!res.ok) {
|
|
2359
2556
|
connectError = `proxy responded ${res.status}`;
|
|
2360
2557
|
}
|
|
@@ -2408,6 +2605,18 @@ async function usage() {
|
|
|
2408
2605
|
for (const line of formatLedgerSummary(lifetime))
|
|
2409
2606
|
console.log(line);
|
|
2410
2607
|
console.log('');
|
|
2608
|
+
// --by-key: the same number split by consumer — named key, header, or
|
|
2609
|
+
// hashed user id (v6.8, dario#1318).
|
|
2610
|
+
const consumers = Object.keys(lifetime.perConsumer ?? {}).length;
|
|
2611
|
+
if (args.includes('--by-key')) {
|
|
2612
|
+
for (const line of formatLedgerConsumers(lifetime))
|
|
2613
|
+
console.log(line);
|
|
2614
|
+
console.log('');
|
|
2615
|
+
}
|
|
2616
|
+
else if (consumers > 0) {
|
|
2617
|
+
console.log(` ${consumers} consumer${consumers === 1 ? '' : 's'} named — \`dario usage --by-key\` splits the number per key.`);
|
|
2618
|
+
console.log('');
|
|
2619
|
+
}
|
|
2411
2620
|
}
|
|
2412
2621
|
else if (lifetimeNote) {
|
|
2413
2622
|
console.log(` API-equivalent spend: ${lifetimeNote}.`);
|
|
@@ -2612,6 +2821,7 @@ const commands = {
|
|
|
2612
2821
|
resume,
|
|
2613
2822
|
logout,
|
|
2614
2823
|
accounts,
|
|
2824
|
+
keys,
|
|
2615
2825
|
add,
|
|
2616
2826
|
codex,
|
|
2617
2827
|
backend,
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export declare const KEYS_VERSION = 1;
|
|
2
|
+
export declare const KEY_PREFIX = "dk_";
|
|
3
|
+
/** Same charset as a pool alias — a key name is printed next to one. */
|
|
4
|
+
export declare const KEY_NAME_RE: RegExp;
|
|
5
|
+
export declare const KEYS_FLUSH_DELAY_MS = 3000;
|
|
6
|
+
export interface KeyRecord {
|
|
7
|
+
/** Stable short id (8 hex), for logs and rotation; never the secret. */
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
/** sha256 hex of the secret. */
|
|
11
|
+
hash: string;
|
|
12
|
+
/** ISO timestamp. */
|
|
13
|
+
created: string;
|
|
14
|
+
/** ISO timestamp of the last request this key authenticated. */
|
|
15
|
+
lastUsed?: string;
|
|
16
|
+
/** Refused, kept for the record. `dario keys revoke` sets it; `list` shows it. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** ISO timestamp; refused after. */
|
|
19
|
+
expires?: string;
|
|
20
|
+
/** Preferred pool seat alias. */
|
|
21
|
+
seat?: string;
|
|
22
|
+
/** Model allowlist: exact ids, or `prefix*`. Empty / absent = any model. */
|
|
23
|
+
models?: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface KeysFile {
|
|
26
|
+
version: number;
|
|
27
|
+
keys: KeyRecord[];
|
|
28
|
+
}
|
|
29
|
+
export declare function keysPathFor(home?: string): string;
|
|
30
|
+
export declare function resolveKeysPath(env?: NodeJS.ProcessEnv): string;
|
|
31
|
+
/**
|
|
32
|
+
* sha256, not a slow KDF, on purpose: a key is 24 bytes from `randomBytes`
|
|
33
|
+
* (192 bits of entropy), never a human-chosen password, so a guess is not
|
|
34
|
+
* a threat a KDF could slow down, and this hash runs once per request on the
|
|
35
|
+
* hot path. This is how GitHub and Stripe store their API tokens.
|
|
36
|
+
* (CodeQL's js/insufficient-password-hash fires on the x-api-key source and
|
|
37
|
+
* is dismissed as a false positive for exactly this reason.)
|
|
38
|
+
*/
|
|
39
|
+
export declare function hashKey(secret: string): string;
|
|
40
|
+
/** `dk_` + 48 hex characters. The prefix lets a reject log say "a named key" without the value. */
|
|
41
|
+
export declare function mintSecret(): string;
|
|
42
|
+
export declare function looksLikeNamedKey(value: string | undefined): boolean;
|
|
43
|
+
export declare function emptyKeysFile(): KeysFile;
|
|
44
|
+
/**
|
|
45
|
+
* Parse a keys file's text, keeping only well-formed records. A file that is
|
|
46
|
+
* not a keys file at all throws; the caller decides whether to move it aside.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseKeysFile(text: string): KeysFile;
|
|
49
|
+
/** Missing file → empty. Unreadable or malformed → throws (never silently empty: that would "revoke" everyone). */
|
|
50
|
+
export declare function readKeysFile(path: string): KeysFile;
|
|
51
|
+
/** Atomic, 0600, parent 0700 — the same primitive config.json uses. */
|
|
52
|
+
export declare function writeKeysFile(path: string, file: KeysFile): void;
|
|
53
|
+
export interface CreateKeyOptions {
|
|
54
|
+
seat?: string;
|
|
55
|
+
models?: string[];
|
|
56
|
+
/** Absolute expiry, epoch ms. */
|
|
57
|
+
expiresAt?: number;
|
|
58
|
+
now?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Mint a key. Returns the record (stored) and the secret (shown once). Mutates `file`. */
|
|
61
|
+
export declare function createKey(file: KeysFile, name: string, opts?: CreateKeyOptions): {
|
|
62
|
+
record: KeyRecord;
|
|
63
|
+
secret: string;
|
|
64
|
+
};
|
|
65
|
+
/** Mark a key refused. Returns false when there is no such key. The record stays, for the list. */
|
|
66
|
+
export declare function revokeKey(file: KeysFile, name: string): boolean;
|
|
67
|
+
/** Forget a key entirely (list no longer shows it). */
|
|
68
|
+
export declare function deleteKey(file: KeysFile, name: string): boolean;
|
|
69
|
+
/** New secret, same name, seat, models and expiry; the old secret stops working at once. */
|
|
70
|
+
export declare function rotateKey(file: KeysFile, name: string, now?: number): {
|
|
71
|
+
record: KeyRecord;
|
|
72
|
+
secret: string;
|
|
73
|
+
} | null;
|
|
74
|
+
export declare function keyIsUsable(k: KeyRecord, now?: number): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The record a presented secret belongs to, or null. Compares hashes in
|
|
77
|
+
* constant time, every record every time, so a miss takes as long as a hit
|
|
78
|
+
* and neither the count of keys nor which one matched leaks through timing.
|
|
79
|
+
* A disabled or expired key matches nothing — the caller cannot tell it from
|
|
80
|
+
* a wrong secret, on purpose.
|
|
81
|
+
*/
|
|
82
|
+
export declare function matchKey(file: KeysFile, provided: string, now?: number): KeyRecord | null;
|
|
83
|
+
/** `models` entries are exact ids, or `prefix*`; case-insensitive. Absent list = any model. */
|
|
84
|
+
export declare function keyAllowsModel(k: Pick<KeyRecord, 'models'>, model: string | null | undefined): boolean;
|
|
85
|
+
/** What `dario keys list` and `GET /admin/keys` show: everything but the hash. */
|
|
86
|
+
export interface KeyPublic {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
created: string;
|
|
90
|
+
last_used: string | null;
|
|
91
|
+
status: 'active' | 'revoked' | 'expired';
|
|
92
|
+
expires: string | null;
|
|
93
|
+
seat: string | null;
|
|
94
|
+
models: string[];
|
|
95
|
+
}
|
|
96
|
+
export declare function publicKey(k: KeyRecord, now?: number): KeyPublic;
|
|
97
|
+
/** `--expires=30d` / `12h` / `2026-12-31` → epoch ms, or null when unparseable. */
|
|
98
|
+
export declare function parseExpiry(value: string, now?: number): number | null;
|
|
99
|
+
/**
|
|
100
|
+
* The proxy's live view of the file. Reloads when the file's mtime moves (a
|
|
101
|
+
* stat per auth — the cost of "no restart"), records last-used with a
|
|
102
|
+
* debounced write that re-reads first so it never clobbers an edit the CLI
|
|
103
|
+
* made in between.
|
|
104
|
+
*/
|
|
105
|
+
export declare class KeyStore {
|
|
106
|
+
readonly path: string;
|
|
107
|
+
private file;
|
|
108
|
+
private mtimeMs;
|
|
109
|
+
private loadError;
|
|
110
|
+
private dirtyLastUsed;
|
|
111
|
+
private flushTimer;
|
|
112
|
+
constructor(path: string);
|
|
113
|
+
/** Read the file now. A malformed file is reported and leaves the last good state in place. */
|
|
114
|
+
load(): void;
|
|
115
|
+
get error(): string | null;
|
|
116
|
+
size(): number;
|
|
117
|
+
list(now?: number): KeyPublic[];
|
|
118
|
+
/** The record a request's credential names, or null. Reloads first when the file moved. */
|
|
119
|
+
match(provided: string | undefined, now?: number): KeyRecord | null;
|
|
120
|
+
/** Note a use; written to disk after a quiet moment. */
|
|
121
|
+
touch(k: KeyRecord, now?: number): void;
|
|
122
|
+
/** Apply a mutation to the file on disk (re-read first), then adopt it. */
|
|
123
|
+
mutate<T>(fn: (file: KeysFile) => T): T;
|
|
124
|
+
/** Persist pending last-used stamps. Safe to call at any time; a no-op when nothing is pending. */
|
|
125
|
+
flush(): void;
|
|
126
|
+
close(): void;
|
|
127
|
+
}
|