@askalf/dario 3.16.0 → 3.19.1
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 +171 -169
- package/dist/accounts.d.ts +2 -0
- package/dist/accounts.js +54 -4
- package/dist/cc-template-data.json +1 -0
- package/dist/cc-template.d.ts +3 -0
- package/dist/cc-template.js +95 -35
- package/dist/cli.js +19 -0
- package/dist/doctor.d.ts +43 -0
- package/dist/doctor.js +208 -0
- package/dist/live-fingerprint.d.ts +137 -0
- package/dist/live-fingerprint.js +375 -9
- package/dist/openai-backend.js +24 -3
- package/dist/proxy.js +108 -16
- package/package.json +2 -2
package/dist/live-fingerprint.js
CHANGED
|
@@ -84,13 +84,20 @@
|
|
|
84
84
|
* contributor — or dario maintainer six months from now — can pick up
|
|
85
85
|
* the right piece without re-deriving the threat model.
|
|
86
86
|
*/
|
|
87
|
-
import { spawn } from 'node:child_process';
|
|
87
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
88
88
|
import { createServer } from 'node:http';
|
|
89
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
89
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
90
90
|
import { homedir } from 'node:os';
|
|
91
91
|
import { join, dirname } from 'node:path';
|
|
92
92
|
import { fileURLToPath } from 'node:url';
|
|
93
93
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
/**
|
|
95
|
+
* Cache-file schema version. Bump when `TemplateData` gains a required
|
|
96
|
+
* field or changes shape in a way that would make older caches produce
|
|
97
|
+
* wrong behavior if loaded verbatim. Mismatched caches are rejected at
|
|
98
|
+
* load time so the fallback + next background refresh write a fresh one.
|
|
99
|
+
*/
|
|
100
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
94
101
|
const LIVE_CACHE = join(homedir(), '.dario', 'cc-template.live.json');
|
|
95
102
|
const LIVE_TTL_MS = 24 * 60 * 60 * 1000; // re-extract once a day
|
|
96
103
|
/**
|
|
@@ -162,20 +169,87 @@ function loadBundledTemplate() {
|
|
|
162
169
|
function readLiveCache() {
|
|
163
170
|
if (!existsSync(LIVE_CACHE))
|
|
164
171
|
return null;
|
|
172
|
+
let raw;
|
|
165
173
|
try {
|
|
166
|
-
|
|
167
|
-
if (!data.system_prompt || !Array.isArray(data.tools) || data.tools.length === 0)
|
|
168
|
-
return null;
|
|
169
|
-
data._source = 'live';
|
|
170
|
-
return data;
|
|
174
|
+
raw = readFileSync(LIVE_CACHE, 'utf-8');
|
|
171
175
|
}
|
|
172
176
|
catch {
|
|
173
177
|
return null;
|
|
174
178
|
}
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(raw);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Unparseable JSON — typically a crash or power-loss mid-write on a
|
|
185
|
+
// pre-v3.17 dario that still used a non-atomic writer. Quarantine
|
|
186
|
+
// the bad file so the next refresh can write a clean one, and log
|
|
187
|
+
// loudly so the user doesn't silently sit on a broken cache forever.
|
|
188
|
+
quarantineCorruptCache(`unparseable JSON (${err.message})`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!parsed || !parsed.system_prompt || !Array.isArray(parsed.tools) || parsed.tools.length === 0) {
|
|
192
|
+
quarantineCorruptCache('missing required fields (system_prompt / tools)');
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
// Schema version mismatch is NOT corruption — it's an expected event on
|
|
196
|
+
// dario upgrade or downgrade. Skip the cache silently; the background
|
|
197
|
+
// refresh will rewrite it in the new shape.
|
|
198
|
+
if (parsed._schemaVersion !== CURRENT_SCHEMA_VERSION)
|
|
199
|
+
return null;
|
|
200
|
+
parsed._source = 'live';
|
|
201
|
+
return parsed;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Rename a corrupt cache file aside to `.corrupt-<ISO>` so the next
|
|
205
|
+
* refresh writes a fresh cache without first having to overwrite a bad
|
|
206
|
+
* file. Keeping the original as-is would also work, but quarantining
|
|
207
|
+
* makes it clearer in `ls ~/.dario` that the file was rejected, and
|
|
208
|
+
* preserves the contents for post-mortem in case a user files an issue.
|
|
209
|
+
*/
|
|
210
|
+
function quarantineCorruptCache(reason) {
|
|
211
|
+
try {
|
|
212
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
213
|
+
const aside = `${LIVE_CACHE}.corrupt-${stamp}`;
|
|
214
|
+
renameSync(LIVE_CACHE, aside);
|
|
215
|
+
console.error(`[dario] ⚠ live template cache rejected: ${reason}. Quarantined to ${aside}. Next background refresh will re-capture.`);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// If the rename itself fails, leave the file in place — a subsequent
|
|
219
|
+
// refresh will overwrite it atomically. Log so the state is visible.
|
|
220
|
+
console.error(`[dario] ⚠ live template cache rejected: ${reason}. (quarantine rename failed: ${err.message})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Atomic JSON write: dump to a sibling `.tmp` file, then rename over the
|
|
225
|
+
* target path. A crash or Ctrl+C between writes never leaves a half-
|
|
226
|
+
* written file where `JSON.parse` would throw on next read. Uses a pid-
|
|
227
|
+
* qualified tmp name so concurrent dario processes don't stomp on each
|
|
228
|
+
* other's partial writes. Exposed for tests via `_atomicWriteJsonForTest`.
|
|
229
|
+
*/
|
|
230
|
+
function atomicWriteJson(targetPath, data) {
|
|
231
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
232
|
+
const tmp = `${targetPath}.${process.pid}.tmp`;
|
|
233
|
+
try {
|
|
234
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
235
|
+
renameSync(tmp, targetPath);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
// Clean up the stray tmp if the rename failed; swallow its own
|
|
239
|
+
// unlink error — nothing useful to do with it.
|
|
240
|
+
try {
|
|
241
|
+
unlinkSync(tmp);
|
|
242
|
+
}
|
|
243
|
+
catch { /* noop */ }
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Test-only surface for `atomicWriteJson`. Production code uses `writeLiveCache`. */
|
|
248
|
+
export function _atomicWriteJsonForTest(targetPath, data) {
|
|
249
|
+
atomicWriteJson(targetPath, data);
|
|
175
250
|
}
|
|
176
251
|
function writeLiveCache(data) {
|
|
177
|
-
|
|
178
|
-
writeFileSync(LIVE_CACHE, JSON.stringify(data, null, 2));
|
|
252
|
+
atomicWriteJson(LIVE_CACHE, data);
|
|
179
253
|
}
|
|
180
254
|
/**
|
|
181
255
|
* Run a loopback MITM server on a random port, spawn CC with
|
|
@@ -302,6 +376,14 @@ async function runCapture(timeoutMs) {
|
|
|
302
376
|
settle(null);
|
|
303
377
|
return;
|
|
304
378
|
}
|
|
379
|
+
// Node 20+ won't spawn `.cmd`/`.bat` without `shell: true` (CVE-2024-27980).
|
|
380
|
+
// `useShell` triggers cmd.exe on Windows — reject overrides that carry
|
|
381
|
+
// shell metacharacters before the spawn, same guard as probeInstalledCCVersion.
|
|
382
|
+
const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(claudeBin);
|
|
383
|
+
if (useShell && /[&|><^"'%\r\n`$;(){}[\]]/.test(claudeBin)) {
|
|
384
|
+
settle(null);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
305
387
|
try {
|
|
306
388
|
child = spawn(claudeBin, ['--print', '-p', 'hi'], {
|
|
307
389
|
env: {
|
|
@@ -313,6 +395,7 @@ async function runCapture(timeoutMs) {
|
|
|
313
395
|
},
|
|
314
396
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
315
397
|
windowsHide: true,
|
|
398
|
+
shell: useShell,
|
|
316
399
|
});
|
|
317
400
|
child.on('error', () => settle(null));
|
|
318
401
|
child.on('exit', () => {
|
|
@@ -331,6 +414,17 @@ async function runCapture(timeoutMs) {
|
|
|
331
414
|
setTimeout(() => settle(captured), timeoutMs);
|
|
332
415
|
});
|
|
333
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Locate the installed `claude` binary and its version. Thin public
|
|
419
|
+
* wrapper over `findClaudeBinary` + `probeInstalledCCVersion` — the
|
|
420
|
+
* doctor CLI and external callers use this to report install state
|
|
421
|
+
* without reaching into module-private helpers.
|
|
422
|
+
*/
|
|
423
|
+
export function findInstalledCC() {
|
|
424
|
+
const path = findClaudeBinary();
|
|
425
|
+
const version = path ? probeInstalledCCVersion() : null;
|
|
426
|
+
return { path, version };
|
|
427
|
+
}
|
|
334
428
|
function findClaudeBinary() {
|
|
335
429
|
// Honor an explicit override first — useful for tests and for users on
|
|
336
430
|
// non-standard installs.
|
|
@@ -394,15 +488,287 @@ export function extractTemplate(captured) {
|
|
|
394
488
|
return null;
|
|
395
489
|
const version = extractCCVersion(captured.headers) ?? 'unknown';
|
|
396
490
|
const headerOrder = extractHeaderOrder(captured.rawHeaders);
|
|
491
|
+
const anthropicBeta = captured.headers['anthropic-beta'];
|
|
492
|
+
const headerValues = extractStaticHeaderValues(captured.headers);
|
|
397
493
|
return {
|
|
398
494
|
_version: version,
|
|
399
495
|
_captured: new Date().toISOString(),
|
|
400
496
|
_source: 'live',
|
|
497
|
+
_schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
401
498
|
agent_identity: agentIdentity,
|
|
402
499
|
system_prompt: systemPrompt,
|
|
403
500
|
tools,
|
|
404
501
|
tool_names: tools.map((t) => t.name),
|
|
405
502
|
header_order: headerOrder,
|
|
503
|
+
anthropic_beta: typeof anthropicBeta === 'string' ? anthropicBeta : undefined,
|
|
504
|
+
header_values: Object.keys(headerValues).length > 0 ? headerValues : undefined,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Pick header values from the captured request that CC would set identically
|
|
509
|
+
* on every outbound call. The replayer overlays these on top of whatever the
|
|
510
|
+
* caller supplied, so anything session-scoped, auth-bearing, or computed by
|
|
511
|
+
* the HTTP stack itself must be excluded.
|
|
512
|
+
*/
|
|
513
|
+
const STATIC_HEADER_EXCLUDE = new Set([
|
|
514
|
+
// Auth — never replay across identities
|
|
515
|
+
'authorization',
|
|
516
|
+
// Body-framing — computed per request
|
|
517
|
+
'content-type', 'content-length', 'transfer-encoding',
|
|
518
|
+
// Host / connection — managed by the HTTP stack
|
|
519
|
+
'host', 'connection', 'keep-alive', 'accept-encoding',
|
|
520
|
+
// Session / request identifiers — rotate per call
|
|
521
|
+
'x-claude-code-session-id', 'x-client-request-id', 'x-request-id',
|
|
522
|
+
// Beta flag is captured separately
|
|
523
|
+
'anthropic-beta',
|
|
524
|
+
// Billing tag — rebuilt per request from cc_version
|
|
525
|
+
'x-anthropic-billing-header',
|
|
526
|
+
]);
|
|
527
|
+
function extractStaticHeaderValues(headers) {
|
|
528
|
+
const out = {};
|
|
529
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
530
|
+
const lk = k.toLowerCase();
|
|
531
|
+
if (STATIC_HEADER_EXCLUDE.has(lk))
|
|
532
|
+
continue;
|
|
533
|
+
if (typeof v !== 'string')
|
|
534
|
+
continue;
|
|
535
|
+
out[lk] = v;
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
// ============================================================
|
|
540
|
+
// Drift detection + startup diagnostics (v3.17)
|
|
541
|
+
// ============================================================
|
|
542
|
+
let _installedVersionProbe = { value: null, cached: false };
|
|
543
|
+
/**
|
|
544
|
+
* Sync-probe `claude --version` and return the parsed version string, e.g.
|
|
545
|
+
* `"2.1.104"`. Memoized per-process — the binary is invoked at most once,
|
|
546
|
+
* subsequent calls return the cached result. Returns `null` if the binary
|
|
547
|
+
* isn't on PATH, or the probe failed / timed out, or the output didn't
|
|
548
|
+
* match the expected format.
|
|
549
|
+
*
|
|
550
|
+
* Used by `detectDrift` to compare the installed CC against the version
|
|
551
|
+
* recorded in the cache at capture time.
|
|
552
|
+
*/
|
|
553
|
+
export function probeInstalledCCVersion() {
|
|
554
|
+
if (_installedVersionProbe.cached)
|
|
555
|
+
return _installedVersionProbe.value;
|
|
556
|
+
const value = probeInstalledCCVersionUncached();
|
|
557
|
+
_installedVersionProbe = { value, cached: true };
|
|
558
|
+
return value;
|
|
559
|
+
}
|
|
560
|
+
function probeInstalledCCVersionUncached() {
|
|
561
|
+
const bin = findClaudeBinary();
|
|
562
|
+
if (!bin)
|
|
563
|
+
return null;
|
|
564
|
+
try {
|
|
565
|
+
// Node 20+ refuses to spawn `.cmd`/`.bat` via execFile without
|
|
566
|
+
// explicit `shell: true` (CVE-2024-27980 hardening). On Windows,
|
|
567
|
+
// npm-installed CLIs commonly live behind a `.cmd` shim — detect
|
|
568
|
+
// that and opt into the shell path.
|
|
569
|
+
//
|
|
570
|
+
// `bin` is normally from findClaudeBinary's fixed allow-list, but
|
|
571
|
+
// DARIO_CLAUDE_BIN lets users override it. If that override reaches
|
|
572
|
+
// the shell path, cmd.exe interprets its contents — so reject any
|
|
573
|
+
// override that carries shell metacharacters before we spawn.
|
|
574
|
+
const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
|
|
575
|
+
if (useShell && /[&|><^"'%\r\n`$;(){}[\]]/.test(bin)) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const out = execFileSync(bin, ['--version'], {
|
|
579
|
+
encoding: 'utf-8',
|
|
580
|
+
timeout: 2_000,
|
|
581
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
582
|
+
windowsHide: true,
|
|
583
|
+
shell: useShell,
|
|
584
|
+
});
|
|
585
|
+
// `claude --version` currently prints e.g. `1.0.79 (Claude Code)` or
|
|
586
|
+
// `claude-cli 2.1.104`. Accept anything that contains a dotted numeric
|
|
587
|
+
// version — the first match wins.
|
|
588
|
+
const m = /(\d+\.\d+\.\d+(?:[.\-][\w.\-]+)?)/.exec(out);
|
|
589
|
+
return m ? m[1] : null;
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Format how old a captured timestamp is, human-readable. `_captured` is
|
|
597
|
+
* an ISO string written by `extractTemplate` or the bundled snapshot.
|
|
598
|
+
* Falls back to `"unknown age"` if the timestamp doesn't parse.
|
|
599
|
+
*/
|
|
600
|
+
export function formatCaptureAge(capturedIso, now = Date.now()) {
|
|
601
|
+
const t = Date.parse(capturedIso);
|
|
602
|
+
if (!Number.isFinite(t))
|
|
603
|
+
return 'unknown age';
|
|
604
|
+
const ageMs = Math.max(0, now - t);
|
|
605
|
+
const s = Math.floor(ageMs / 1000);
|
|
606
|
+
if (s < 60)
|
|
607
|
+
return `${s}s`;
|
|
608
|
+
const m = Math.floor(s / 60);
|
|
609
|
+
if (m < 60)
|
|
610
|
+
return `${m}m`;
|
|
611
|
+
const h = Math.floor(m / 60);
|
|
612
|
+
if (h < 48)
|
|
613
|
+
return `${h}h`;
|
|
614
|
+
const d = Math.floor(h / 24);
|
|
615
|
+
return `${d}d`;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* One-line human summary of the active template — what source, which CC
|
|
619
|
+
* version captured it, and how old that capture is. Proxy and shim
|
|
620
|
+
* startup log this so users can tell at a glance whether they're on a
|
|
621
|
+
* fresh live capture or a stale bundled fallback.
|
|
622
|
+
*/
|
|
623
|
+
export function describeTemplate(t) {
|
|
624
|
+
const source = t._source ?? 'bundled';
|
|
625
|
+
const age = formatCaptureAge(t._captured);
|
|
626
|
+
return `${source} capture, CC v${t._version} (${age} old)`;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Compare the loaded template's captured CC version against the version
|
|
630
|
+
* reported by `claude --version` on the current machine. Drifted caches
|
|
631
|
+
* are still usable — the shape is probably compatible — but the proxy
|
|
632
|
+
* should force-refresh ASAP so the next startup is back in sync.
|
|
633
|
+
*
|
|
634
|
+
* @param installedOverride test-only injection for unit tests; production
|
|
635
|
+
* callers pass nothing and the real binary probe runs.
|
|
636
|
+
*/
|
|
637
|
+
export function detectDrift(t, installedOverride) {
|
|
638
|
+
const installed = installedOverride !== undefined ? installedOverride : probeInstalledCCVersion();
|
|
639
|
+
const cachedVersion = t._version;
|
|
640
|
+
if (installed === null) {
|
|
641
|
+
return {
|
|
642
|
+
drifted: false,
|
|
643
|
+
cachedVersion,
|
|
644
|
+
installedVersion: null,
|
|
645
|
+
message: 'installed CC version not probed (binary not on PATH or probe failed)',
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (installed === cachedVersion) {
|
|
649
|
+
return {
|
|
650
|
+
drifted: false,
|
|
651
|
+
cachedVersion,
|
|
652
|
+
installedVersion: installed,
|
|
653
|
+
message: `cache matches installed CC (v${installed})`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
drifted: true,
|
|
658
|
+
cachedVersion,
|
|
659
|
+
installedVersion: installed,
|
|
660
|
+
message: `cache is from CC v${cachedVersion} but installed CC is v${installed} — background refresh will re-capture`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Reset the memoized `claude --version` probe. Test-only — production
|
|
665
|
+
* code should never need to clear the cache since the installed binary
|
|
666
|
+
* doesn't change mid-process.
|
|
667
|
+
*/
|
|
668
|
+
export function _resetInstalledVersionProbeForTest() {
|
|
669
|
+
_installedVersionProbe = { value: null, cached: false };
|
|
670
|
+
}
|
|
671
|
+
// ============================================================
|
|
672
|
+
// CC version compat matrix (v3.17)
|
|
673
|
+
// ============================================================
|
|
674
|
+
/**
|
|
675
|
+
* The CC version range the current dario release has been exercised
|
|
676
|
+
* against. Update `maxTested` every time we validate against a new CC
|
|
677
|
+
* (ideally as part of the release checklist — the e2e test against the
|
|
678
|
+
* user's own CC is the ground-truth signal).
|
|
679
|
+
*
|
|
680
|
+
* - `min`: below this, dario's extractor hasn't been validated; proxy
|
|
681
|
+
* will still run but may mis-parse CC's request body.
|
|
682
|
+
* - `maxTested`: the newest CC version the current dario release has
|
|
683
|
+
* been exercised against. Above this, dario is *likely* fine (CC's
|
|
684
|
+
* request shape evolves slowly) but it's explicitly untested, so
|
|
685
|
+
* users get a soft warn and we get a signal to refresh the bundled
|
|
686
|
+
* snapshot + rerun e2e.
|
|
687
|
+
*/
|
|
688
|
+
export const SUPPORTED_CC_RANGE = {
|
|
689
|
+
min: '1.0.0',
|
|
690
|
+
maxTested: '2.1.104',
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
|
694
|
+
* zero if equal, positive if `a>b`. Handles suffixes like `-beta.1` or
|
|
695
|
+
* `.dev` by comparing the numeric prefix first and treating anything
|
|
696
|
+
* after as a tiebreaker (strings compared lexicographically; absence of
|
|
697
|
+
* suffix beats presence, matching semver's "release > prerelease").
|
|
698
|
+
*
|
|
699
|
+
* Intentionally minimal — dario's "zero runtime deps" policy rules out
|
|
700
|
+
* pulling `semver`. CC versions are well-formed `M.m.p[-suffix]` so we
|
|
701
|
+
* don't need the full spec.
|
|
702
|
+
*/
|
|
703
|
+
export function compareVersions(a, b) {
|
|
704
|
+
const splitPrefixSuffix = (v) => {
|
|
705
|
+
const m = /^(\d+(?:\.\d+)*)(.*)$/.exec(v);
|
|
706
|
+
if (!m)
|
|
707
|
+
return { parts: [0], suffix: v };
|
|
708
|
+
const parts = m[1].split('.').map((s) => parseInt(s, 10));
|
|
709
|
+
return { parts, suffix: m[2] ?? '' };
|
|
710
|
+
};
|
|
711
|
+
const A = splitPrefixSuffix(a);
|
|
712
|
+
const B = splitPrefixSuffix(b);
|
|
713
|
+
const len = Math.max(A.parts.length, B.parts.length);
|
|
714
|
+
for (let i = 0; i < len; i++) {
|
|
715
|
+
const ai = A.parts[i] ?? 0;
|
|
716
|
+
const bi = B.parts[i] ?? 0;
|
|
717
|
+
if (ai !== bi)
|
|
718
|
+
return ai - bi;
|
|
719
|
+
}
|
|
720
|
+
// Numeric prefix equal — compare suffix. Empty suffix beats non-empty
|
|
721
|
+
// (release > prerelease). Otherwise lexicographic.
|
|
722
|
+
if (A.suffix === B.suffix)
|
|
723
|
+
return 0;
|
|
724
|
+
if (A.suffix === '')
|
|
725
|
+
return 1;
|
|
726
|
+
if (B.suffix === '')
|
|
727
|
+
return -1;
|
|
728
|
+
return A.suffix < B.suffix ? -1 : 1;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Check whether the installed CC version sits inside the supported range.
|
|
732
|
+
* Called at startup by the proxy; the result drives whether we emit a
|
|
733
|
+
* compatibility warning to the user.
|
|
734
|
+
*
|
|
735
|
+
* `unknown` is not a failure — it just means we couldn't probe (no CC on
|
|
736
|
+
* PATH, timeout, parse miss). Dario still runs on bundled template.
|
|
737
|
+
*
|
|
738
|
+
* @param installedOverride test-only injection; production callers pass nothing.
|
|
739
|
+
*/
|
|
740
|
+
export function checkCCCompat(installedOverride) {
|
|
741
|
+
const installed = installedOverride !== undefined ? installedOverride : probeInstalledCCVersion();
|
|
742
|
+
const range = { min: SUPPORTED_CC_RANGE.min, maxTested: SUPPORTED_CC_RANGE.maxTested };
|
|
743
|
+
if (installed === null) {
|
|
744
|
+
return {
|
|
745
|
+
status: 'unknown',
|
|
746
|
+
installedVersion: null,
|
|
747
|
+
range,
|
|
748
|
+
message: 'installed CC version not probed — compatibility unchecked',
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (compareVersions(installed, range.min) < 0) {
|
|
752
|
+
return {
|
|
753
|
+
status: 'below-min',
|
|
754
|
+
installedVersion: installed,
|
|
755
|
+
range,
|
|
756
|
+
message: `installed CC v${installed} is older than the minimum dario supports (v${range.min}); extractor may mis-parse requests — upgrade CC`,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (compareVersions(installed, range.maxTested) > 0) {
|
|
760
|
+
return {
|
|
761
|
+
status: 'untested-above',
|
|
762
|
+
installedVersion: installed,
|
|
763
|
+
range,
|
|
764
|
+
message: `installed CC v${installed} is newer than dario's last tested version (v${range.maxTested}); usually fine, but untested`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
status: 'ok',
|
|
769
|
+
installedVersion: installed,
|
|
770
|
+
range,
|
|
771
|
+
message: `installed CC v${installed} is within the tested range (v${range.min} – v${range.maxTested})`,
|
|
406
772
|
};
|
|
407
773
|
}
|
|
408
774
|
/**
|
package/dist/openai-backend.js
CHANGED
|
@@ -13,10 +13,27 @@
|
|
|
13
13
|
* in a follow-up release.
|
|
14
14
|
*/
|
|
15
15
|
import { readFile, writeFile, mkdir, unlink, readdir } from 'node:fs/promises';
|
|
16
|
-
import { join } from 'node:path';
|
|
16
|
+
import { join, basename } from 'node:path';
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
18
|
const DARIO_DIR = join(homedir(), '.dario');
|
|
19
19
|
const BACKENDS_DIR = join(DARIO_DIR, 'backends');
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a caller-supplied backend name into a filesystem-safe leaf.
|
|
22
|
+
* Strips any directory component and rejects names outside the allowed
|
|
23
|
+
* charset. Defense in depth — CLI input is already constrained.
|
|
24
|
+
*/
|
|
25
|
+
function safeBackendPath(name) {
|
|
26
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const leaf = basename(name);
|
|
29
|
+
if (leaf !== name)
|
|
30
|
+
return null;
|
|
31
|
+
if (leaf === '.' || leaf === '..')
|
|
32
|
+
return null;
|
|
33
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_\-.]{0,63}$/.test(leaf))
|
|
34
|
+
return null;
|
|
35
|
+
return join(BACKENDS_DIR, `${leaf}.json`);
|
|
36
|
+
}
|
|
20
37
|
async function ensureDir() {
|
|
21
38
|
await mkdir(BACKENDS_DIR, { recursive: true, mode: 0o700 });
|
|
22
39
|
}
|
|
@@ -40,12 +57,16 @@ export async function listBackends() {
|
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
export async function saveBackend(creds) {
|
|
60
|
+
const path = safeBackendPath(creds.name);
|
|
61
|
+
if (!path)
|
|
62
|
+
throw new Error(`invalid backend name: ${creds.name}`);
|
|
43
63
|
await ensureDir();
|
|
44
|
-
const path = join(BACKENDS_DIR, `${creds.name}.json`);
|
|
45
64
|
await writeFile(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
46
65
|
}
|
|
47
66
|
export async function removeBackend(name) {
|
|
48
|
-
const path =
|
|
67
|
+
const path = safeBackendPath(name);
|
|
68
|
+
if (!path)
|
|
69
|
+
return false;
|
|
49
70
|
try {
|
|
50
71
|
await unlink(path);
|
|
51
72
|
return true;
|