@askalf/dario 5.2.10 → 5.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.d.ts +33 -0
- package/dist/accounts.js +51 -1
- package/dist/cc-template-data.json +8 -3
- package/dist/cli.js +8 -6
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/oauth.d.ts +9 -0
- package/dist/oauth.js +11 -0
- package/dist/proxy.js +24 -1
- package/dist/runtime-fingerprint.d.ts +27 -1
- package/dist/runtime-fingerprint.js +47 -0
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -160,3 +160,36 @@ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<st
|
|
|
160
160
|
* back-filled snapshot.
|
|
161
161
|
*/
|
|
162
162
|
export declare function resyncLoginFromCredentialsIfStale(): Promise<'no-pool' | 'no-login' | 'no-creds' | 'in-sync' | 'resynced' | 'creds-stale'>;
|
|
163
|
+
/**
|
|
164
|
+
* Mirror the pool's freshly-refreshed `login` account token back into the
|
|
165
|
+
* legacy `~/.dario/credentials.json` store. dario#808.
|
|
166
|
+
*
|
|
167
|
+
* The divergence #805 fixed runs one way (credentials.json stale, login.json
|
|
168
|
+
* fresh) but left the two files diverged: the pool's refresh loop advances
|
|
169
|
+
* accounts/login.json while nothing ever writes credentials.json, so the
|
|
170
|
+
* legacy file stays frozen at the last `dario login`. Consequences:
|
|
171
|
+
* - `dario doctor` reads credentials.json for its OAuth row and prints
|
|
172
|
+
* 'expired'/'expiring' in pool-of-1 mode even when the live pool token is
|
|
173
|
+
* fresh and the fleet is 200-healthy — an alarm-inducing false positive.
|
|
174
|
+
* - any other reader of credentials.json (a co-resident Claude Code, an ops
|
|
175
|
+
* script) sees a stale token indefinitely.
|
|
176
|
+
*
|
|
177
|
+
* Fix: whenever the pool refreshes the `login` account, mirror the new token
|
|
178
|
+
* into credentials.json so the legacy file tracks the pool store. Only the
|
|
179
|
+
* `login` alias is mirrored — it's the one back-filled FROM credentials.json
|
|
180
|
+
* (ensureLoginCredentialsInPool), so it's the only account whose canonical
|
|
181
|
+
* home is that file; `work`/`personal` accounts have no legacy counterpart.
|
|
182
|
+
*
|
|
183
|
+
* Freshness-guarded, symmetric with #805: mirror only when the refreshed login
|
|
184
|
+
* token is strictly NEWER than the current credentials.json (by `expiresAt`).
|
|
185
|
+
* A newer-or-equal credentials.json means another process (e.g. a concurrent
|
|
186
|
+
* `dario login --force-reauth`) just wrote a fresher family — never clobber it;
|
|
187
|
+
* resyncLoginFromCredentialsIfStale pulls that direction on next startup.
|
|
188
|
+
*
|
|
189
|
+
* Best-effort: a mirror failure must never fail the refresh that produced the
|
|
190
|
+
* live token. Returns one of:
|
|
191
|
+
* - 'skip-not-login' : alias isn't the reserved `login` — nothing to mirror
|
|
192
|
+
* - 'mirrored' : credentials.json updated to the pool token
|
|
193
|
+
* - 'creds-newer' : credentials.json is newer-or-equal — left untouched
|
|
194
|
+
*/
|
|
195
|
+
export declare function mirrorLoginToCredentials(refreshed: AccountCredentials): Promise<'skip-not-login' | 'mirrored' | 'creds-newer'>;
|
package/dist/accounts.js
CHANGED
|
@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
|
|
|
23
23
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
24
24
|
import { createServer } from 'node:http';
|
|
25
25
|
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
26
|
-
import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
|
|
26
|
+
import { loadCredentials, saveCredentialsTokens, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
|
|
27
27
|
import { openBrowser } from './open-browser.js';
|
|
28
28
|
import { redactSecrets } from './redact.js';
|
|
29
29
|
import { durableWriteFile } from './durable-write.js';
|
|
@@ -619,3 +619,53 @@ export async function resyncLoginFromCredentialsIfStale() {
|
|
|
619
619
|
});
|
|
620
620
|
return 'resynced';
|
|
621
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Mirror the pool's freshly-refreshed `login` account token back into the
|
|
624
|
+
* legacy `~/.dario/credentials.json` store. dario#808.
|
|
625
|
+
*
|
|
626
|
+
* The divergence #805 fixed runs one way (credentials.json stale, login.json
|
|
627
|
+
* fresh) but left the two files diverged: the pool's refresh loop advances
|
|
628
|
+
* accounts/login.json while nothing ever writes credentials.json, so the
|
|
629
|
+
* legacy file stays frozen at the last `dario login`. Consequences:
|
|
630
|
+
* - `dario doctor` reads credentials.json for its OAuth row and prints
|
|
631
|
+
* 'expired'/'expiring' in pool-of-1 mode even when the live pool token is
|
|
632
|
+
* fresh and the fleet is 200-healthy — an alarm-inducing false positive.
|
|
633
|
+
* - any other reader of credentials.json (a co-resident Claude Code, an ops
|
|
634
|
+
* script) sees a stale token indefinitely.
|
|
635
|
+
*
|
|
636
|
+
* Fix: whenever the pool refreshes the `login` account, mirror the new token
|
|
637
|
+
* into credentials.json so the legacy file tracks the pool store. Only the
|
|
638
|
+
* `login` alias is mirrored — it's the one back-filled FROM credentials.json
|
|
639
|
+
* (ensureLoginCredentialsInPool), so it's the only account whose canonical
|
|
640
|
+
* home is that file; `work`/`personal` accounts have no legacy counterpart.
|
|
641
|
+
*
|
|
642
|
+
* Freshness-guarded, symmetric with #805: mirror only when the refreshed login
|
|
643
|
+
* token is strictly NEWER than the current credentials.json (by `expiresAt`).
|
|
644
|
+
* A newer-or-equal credentials.json means another process (e.g. a concurrent
|
|
645
|
+
* `dario login --force-reauth`) just wrote a fresher family — never clobber it;
|
|
646
|
+
* resyncLoginFromCredentialsIfStale pulls that direction on next startup.
|
|
647
|
+
*
|
|
648
|
+
* Best-effort: a mirror failure must never fail the refresh that produced the
|
|
649
|
+
* live token. Returns one of:
|
|
650
|
+
* - 'skip-not-login' : alias isn't the reserved `login` — nothing to mirror
|
|
651
|
+
* - 'mirrored' : credentials.json updated to the pool token
|
|
652
|
+
* - 'creds-newer' : credentials.json is newer-or-equal — left untouched
|
|
653
|
+
*/
|
|
654
|
+
export async function mirrorLoginToCredentials(refreshed) {
|
|
655
|
+
if (refreshed.alias !== MIGRATED_LOGIN_ALIAS)
|
|
656
|
+
return 'skip-not-login';
|
|
657
|
+
const creds = await loadCredentials();
|
|
658
|
+
const currentExpiry = creds?.claudeAiOauth?.expiresAt ?? 0;
|
|
659
|
+
// Only mirror a strictly-newer pool token. Equal expiry means the files
|
|
660
|
+
// already agree (or a same-second write elsewhere); newer credentials.json
|
|
661
|
+
// is another process's fresher family we must not overwrite (#805 direction).
|
|
662
|
+
if (currentExpiry >= refreshed.expiresAt)
|
|
663
|
+
return 'creds-newer';
|
|
664
|
+
await saveCredentialsTokens({
|
|
665
|
+
accessToken: refreshed.accessToken,
|
|
666
|
+
refreshToken: refreshed.refreshToken,
|
|
667
|
+
expiresAt: refreshed.expiresAt,
|
|
668
|
+
scopes: refreshed.scopes ?? creds?.claudeAiOauth?.scopes ?? [],
|
|
669
|
+
});
|
|
670
|
+
return 'mirrored';
|
|
671
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_version": "2.1.214",
|
|
3
|
-
"_captured": "2026-07-
|
|
3
|
+
"_captured": "2026-07-18T14:14:16.825Z",
|
|
4
4
|
"_source": "bundled",
|
|
5
5
|
"_schemaVersion": 3,
|
|
6
6
|
"agent_identity": "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"name": "Agent",
|
|
11
|
-
"description": "Launch a new agent to handle complex, multi-step tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types are listed in <system-reminder> messages in the conversation.\n\nWhen using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.\n\n## When to use\n\nReach for this when the task matches an available agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate it and you keep the conclusion, not the file dumps. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you've delegated a search, don't also run it yourself — wait for the result.\n\n- The agent's final
|
|
11
|
+
"description": "Launch a new agent to handle complex, multi-step tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types are listed in <system-reminder> messages in the conversation.\n\nWhen using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.\n\n## When to use\n\nReach for this when the task matches an available agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate it and you keep the conclusion, not the file dumps. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you've delegated a search, don't also run it yourself — wait for the result.\n\n- The agent's final report is not shown to the user — relay what matters.\n- Use SendMessage with the agent's ID or name to continue a previously spawned agent with its context intact; a new Agent call starts fresh.\n- Each agent type's model, reasoning effort, and tools come from its definition (`.claude/agents/*.md` frontmatter or SDK `agents`).\n- `isolation: \"worktree\"` gives the agent its own git worktree (auto-cleaned if unchanged).\n- Subagents run in the background by default; you'll be notified when one completes. Pass `run_in_background: false` for a synchronous run when you need the result before continuing. Never fabricate or predict a pending agent's results — the notification is never something you write yourself; if the user asks before it arrives, say it's still running.",
|
|
12
12
|
"input_schema": {
|
|
13
13
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
14
14
|
"type": "object",
|
|
@@ -939,6 +939,11 @@
|
|
|
939
939
|
"description": "One-sentence statement of the defect",
|
|
940
940
|
"type": "string"
|
|
941
941
|
},
|
|
942
|
+
"short_summary": {
|
|
943
|
+
"description": "Compressed label for compact UI (≤60 chars): the claim alone, no rationale or consequence clause",
|
|
944
|
+
"type": "string",
|
|
945
|
+
"maxLength": 60
|
|
946
|
+
},
|
|
942
947
|
"failure_scenario": {
|
|
943
948
|
"description": "Concrete inputs/state → wrong output/crash",
|
|
944
949
|
"type": "string"
|
|
@@ -1038,7 +1043,7 @@
|
|
|
1038
1043
|
},
|
|
1039
1044
|
{
|
|
1040
1045
|
"name": "Skill",
|
|
1041
|
-
"description": "
|
|
1046
|
+
"description": "Invoke a skill.\n\nA skill is a packaged set of instructions the user or project has set up for a particular kind of task (deploy steps, a review checklist, a repo-specific workflow). Available skills appear in a system-reminder listing with one-line descriptions. When the task at hand is one a listed skill covers, call this tool first — the skill's instructions load into the turn for you to follow in place of your default approach; some skills instead run in a subagent and return the finished result. Users may also ask for one by name (`/<name>`, or \"slash command\"); that's a request to invoke it.\n\n- `skill`: exact name from the listing, no leading slash. Plugin skills use `plugin:skill`. Directory-scoped skills are listed with a path prefix (`apps/web:deploy`); when both scoped and unscoped variants of a name exist, pick the one whose directory contains the files you're working on (most specific wins; unscoped otherwise).\n- `args`: optional arguments to pass through.\n\nOnly names from the listing (or that the user typed explicitly) are valid. Built-in CLI commands (`/help`, `/clear`, …) aren't skills. If a `<command-name>` block is already present this turn, the skill is loaded — follow it directly rather than calling again.\n",
|
|
1042
1047
|
"input_schema": {
|
|
1043
1048
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
1044
1049
|
"type": "object",
|
package/dist/cli.js
CHANGED
|
@@ -1287,13 +1287,15 @@ async function help() {
|
|
|
1287
1287
|
intact even when a text-tool client is
|
|
1288
1288
|
detected; use --preserve-tools per session
|
|
1289
1289
|
when edits are needed. (dario#40)
|
|
1290
|
-
--strict-tls Refuse to start proxy mode
|
|
1291
|
-
|
|
1292
|
-
|
|
1290
|
+
--strict-tls Refuse to start proxy mode unless this process
|
|
1291
|
+
runs under Bun at a version whose JA3 is verified
|
|
1292
|
+
to match Claude Code (≥ v1.3.14). Bun is what
|
|
1293
|
+
Claude Code uses; matching its TLS stack keeps the
|
|
1293
1294
|
proxy's JA3/JA4 ClientHello indistinguishable
|
|
1294
|
-
from a stock CC request
|
|
1295
|
-
|
|
1296
|
-
|
|
1295
|
+
from a stock CC request — but an older Bun ships an
|
|
1296
|
+
older BoringSSL whose ClientHello diverges (#813).
|
|
1297
|
+
Install/upgrade Bun (https://bun.sh) so dario
|
|
1298
|
+
auto-relaunches under it. (v3.23)
|
|
1297
1299
|
--stealth Single-flag behavioral-stealth preset.
|
|
1298
1300
|
Flips pace-jitter, think-time, and
|
|
1299
1301
|
session-start defaults from 0 to non-zero
|
|
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
|
|
|
282
282
|
*/
|
|
283
283
|
export declare const SUPPORTED_CC_RANGE: {
|
|
284
284
|
readonly min: "1.0.0";
|
|
285
|
-
readonly maxTested: "2.1.
|
|
285
|
+
readonly maxTested: "2.1.215";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -806,7 +806,7 @@ export function _resetInstalledVersionProbeForTest() {
|
|
|
806
806
|
*/
|
|
807
807
|
export const SUPPORTED_CC_RANGE = {
|
|
808
808
|
min: '1.0.0',
|
|
809
|
-
maxTested: '2.1.
|
|
809
|
+
maxTested: '2.1.215',
|
|
810
810
|
};
|
|
811
811
|
/**
|
|
812
812
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/oauth.d.ts
CHANGED
|
@@ -73,6 +73,15 @@ export declare function loadCredentials(): Promise<CredentialsFile | null>;
|
|
|
73
73
|
* tiebreaker preference. Exported for direct testing.
|
|
74
74
|
*/
|
|
75
75
|
export declare function pickFreshestCredentials(candidates: CredentialsFile[]): CredentialsFile | null;
|
|
76
|
+
/**
|
|
77
|
+
* Mirror a set of OAuth tokens into the legacy `~/.dario/credentials.json`
|
|
78
|
+
* store. Thin durable-write wrapper over `saveCredentials` for callers outside
|
|
79
|
+
* this module — specifically the pool's login-account mirror (dario#808), which
|
|
80
|
+
* keeps credentials.json tracking the pool store after a background/startup
|
|
81
|
+
* refresh advanced `accounts/login.json`. Shares saveCredentials' durability
|
|
82
|
+
* (fsync temp + dir, #790), cache invalidation, and refresh-dead-flag clear.
|
|
83
|
+
*/
|
|
84
|
+
export declare function saveCredentialsTokens(tokens: OAuthTokens): Promise<void>;
|
|
76
85
|
/**
|
|
77
86
|
* Automatic OAuth flow using a local callback server (same as Claude Code).
|
|
78
87
|
* Opens browser, captures the authorization code automatically.
|
package/dist/oauth.js
CHANGED
|
@@ -424,6 +424,17 @@ async function saveCredentials(creds) {
|
|
|
424
424
|
credentialsCache = creds;
|
|
425
425
|
credentialsCacheTime = Date.now();
|
|
426
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Mirror a set of OAuth tokens into the legacy `~/.dario/credentials.json`
|
|
429
|
+
* store. Thin durable-write wrapper over `saveCredentials` for callers outside
|
|
430
|
+
* this module — specifically the pool's login-account mirror (dario#808), which
|
|
431
|
+
* keeps credentials.json tracking the pool store after a background/startup
|
|
432
|
+
* refresh advanced `accounts/login.json`. Shares saveCredentials' durability
|
|
433
|
+
* (fsync temp + dir, #790), cache invalidation, and refresh-dead-flag clear.
|
|
434
|
+
*/
|
|
435
|
+
export async function saveCredentialsTokens(tokens) {
|
|
436
|
+
await saveCredentials({ claudeAiOauth: tokens });
|
|
437
|
+
}
|
|
427
438
|
/**
|
|
428
439
|
* Automatic OAuth flow using a local callback server (same as Claude Code).
|
|
429
440
|
* Opens browser, captures the authorization code automatically.
|
package/dist/proxy.js
CHANGED
|
@@ -16,7 +16,7 @@ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCo
|
|
|
16
16
|
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS } from './analytics.js';
|
|
17
17
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
18
18
|
import { notify as osNotify } from './notify.js';
|
|
19
|
-
import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool } from './accounts.js';
|
|
19
|
+
import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials } from './accounts.js';
|
|
20
20
|
import { handleAdminRequest } from './admin-api.js';
|
|
21
21
|
import { createTokenBucket } from './rate-limit.js';
|
|
22
22
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
@@ -1233,6 +1233,11 @@ export async function startProxy(opts = {}) {
|
|
|
1233
1233
|
return;
|
|
1234
1234
|
const refreshed = await refreshAccountToken(saved);
|
|
1235
1235
|
pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
|
|
1236
|
+
// Mirror a refreshed `login` token back to credentials.json so the
|
|
1237
|
+
// legacy file (and `dario doctor`) tracks the pool store (#808).
|
|
1238
|
+
await mirrorLoginToCredentials(refreshed).catch((err) => {
|
|
1239
|
+
console.error(`[dario] login→credentials.json mirror failed (startup) for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
|
|
1240
|
+
});
|
|
1236
1241
|
console.error(`[dario] Startup refresh recovered account ${acc.alias} (was expired/expiring).`);
|
|
1237
1242
|
}
|
|
1238
1243
|
catch (err) {
|
|
@@ -1253,6 +1258,11 @@ export async function startProxy(opts = {}) {
|
|
|
1253
1258
|
continue;
|
|
1254
1259
|
const refreshed = await refreshAccountToken(saved);
|
|
1255
1260
|
pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
|
|
1261
|
+
// Mirror a refreshed `login` token back to credentials.json so the
|
|
1262
|
+
// legacy file (and `dario doctor`) tracks the pool store (#808).
|
|
1263
|
+
await mirrorLoginToCredentials(refreshed).catch((err) => {
|
|
1264
|
+
console.error(`[dario] login→credentials.json mirror failed (background) for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
|
|
1265
|
+
});
|
|
1256
1266
|
}
|
|
1257
1267
|
catch (err) {
|
|
1258
1268
|
console.error(`[dario] Background refresh failed for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -1628,6 +1638,19 @@ export async function startProxy(opts = {}) {
|
|
|
1628
1638
|
}
|
|
1629
1639
|
// Strip query parameters for endpoint matching
|
|
1630
1640
|
const urlPath = req.url?.split('?')[0] ?? '';
|
|
1641
|
+
// Liveness probe — always 200 while the HTTP server is accepting requests,
|
|
1642
|
+
// deliberately decoupled from OAuth state. Docker's healthcheck (and the
|
|
1643
|
+
// autoheal watchdog that keys on it) points HERE, not /health: a broken or
|
|
1644
|
+
// expired refresh token makes /health return 503, but a container restart
|
|
1645
|
+
// cannot mint a new token, so restarting on that is a pointless loop (one
|
|
1646
|
+
// shared-refresh-family outage thrashed dario for 4h+ this way). Readiness —
|
|
1647
|
+
// the 503-on-broken-OAuth verdict that uptime monitors and
|
|
1648
|
+
// `depends_on: service_healthy` need — stays on /health.
|
|
1649
|
+
if (urlPath === '/livez') {
|
|
1650
|
+
res.writeHead(200, JSON_HEADERS);
|
|
1651
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1631
1654
|
// Health check
|
|
1632
1655
|
//
|
|
1633
1656
|
// Returns HTTP 503 when OAuth is in a state that will cause every upstream
|
|
@@ -25,8 +25,16 @@
|
|
|
25
25
|
*/
|
|
26
26
|
/** Canonical buckets the caller pivots on. */
|
|
27
27
|
export type RuntimeFingerprintStatus =
|
|
28
|
-
/** Running under Bun — TLS
|
|
28
|
+
/** Running under Bun ≥ the JA3-verified floor — TLS ClientHello matches CC. */
|
|
29
29
|
'bun-match'
|
|
30
|
+
/**
|
|
31
|
+
* Running under Bun, but at a version below the JA3-verified floor: being on
|
|
32
|
+
* Bun is necessary but not sufficient. Older Bun ships an older BoringSSL
|
|
33
|
+
* whose ClientHello is not confirmed to match CC's (measured divergent on
|
|
34
|
+
* Bun 1.0.9 — see #813). Treated as a warn so an old Bun on PATH can't
|
|
35
|
+
* report a false-green match while emitting a divergent JA3.
|
|
36
|
+
*/
|
|
37
|
+
| 'bun-ja3-unverified'
|
|
30
38
|
/** Running under Node, Bun available on PATH but auto-relaunch was bypassed. */
|
|
31
39
|
| 'bun-bypassed'
|
|
32
40
|
/** Running under Node, Bun not installed. */
|
|
@@ -54,6 +62,24 @@ export interface RuntimeFingerprint {
|
|
|
54
62
|
* the (~sub-100ms) cost when Bun is installed.
|
|
55
63
|
*/
|
|
56
64
|
export declare function probeBunVersion(): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Lowest public Bun version whose TLS ClientHello (JA3) is *measured* to match
|
|
67
|
+
* the Bun/BoringSSL fingerprint Claude Code presents on the wire. Empirical
|
|
68
|
+
* basis (#813, macOS arm64): CC 2.1.214 embeds the Bun canary line and hashes
|
|
69
|
+
* to JA3 `e97f5146a7009cc2918b50e903b6ff8d`; bare public Bun 1.3.14 and canary
|
|
70
|
+
* reproduce it byte-for-byte, while Bun 1.0.9 diverges (adds 3DES, ECH,
|
|
71
|
+
* padding → `2ae7eb4b…`). The window between 1.0.9 and 1.3.14 is unmeasured,
|
|
72
|
+
* so anything below this floor is reported unverified rather than a green match.
|
|
73
|
+
*/
|
|
74
|
+
export declare const JA3_VERIFIED_BUN_FLOOR = "1.3.14";
|
|
75
|
+
/**
|
|
76
|
+
* True when Bun `version` is at or above `floor`. Parses the leading
|
|
77
|
+
* `major.minor.patch` and ignores any pre-release/`-canary…` suffix, so Bun's
|
|
78
|
+
* canary tags (e.g. `1.4.0-canary.x`) compare as their base triple. Returns
|
|
79
|
+
* `undefined` when either string can't be parsed — the caller decides how to
|
|
80
|
+
* treat "can't tell" (we keep those as a best-effort match rather than warn).
|
|
81
|
+
*/
|
|
82
|
+
export declare function bunVersionMeetsJa3Floor(version: string, floor?: string): boolean | undefined;
|
|
57
83
|
/**
|
|
58
84
|
* Synthesize the TLS-fingerprint status from three inputs. All three are
|
|
59
85
|
* passed explicitly so tests can cover every combination without touching
|
|
@@ -51,6 +51,38 @@ export function probeBunVersion() {
|
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Lowest public Bun version whose TLS ClientHello (JA3) is *measured* to match
|
|
56
|
+
* the Bun/BoringSSL fingerprint Claude Code presents on the wire. Empirical
|
|
57
|
+
* basis (#813, macOS arm64): CC 2.1.214 embeds the Bun canary line and hashes
|
|
58
|
+
* to JA3 `e97f5146a7009cc2918b50e903b6ff8d`; bare public Bun 1.3.14 and canary
|
|
59
|
+
* reproduce it byte-for-byte, while Bun 1.0.9 diverges (adds 3DES, ECH,
|
|
60
|
+
* padding → `2ae7eb4b…`). The window between 1.0.9 and 1.3.14 is unmeasured,
|
|
61
|
+
* so anything below this floor is reported unverified rather than a green match.
|
|
62
|
+
*/
|
|
63
|
+
export const JA3_VERIFIED_BUN_FLOOR = '1.3.14';
|
|
64
|
+
/**
|
|
65
|
+
* True when Bun `version` is at or above `floor`. Parses the leading
|
|
66
|
+
* `major.minor.patch` and ignores any pre-release/`-canary…` suffix, so Bun's
|
|
67
|
+
* canary tags (e.g. `1.4.0-canary.x`) compare as their base triple. Returns
|
|
68
|
+
* `undefined` when either string can't be parsed — the caller decides how to
|
|
69
|
+
* treat "can't tell" (we keep those as a best-effort match rather than warn).
|
|
70
|
+
*/
|
|
71
|
+
export function bunVersionMeetsJa3Floor(version, floor = JA3_VERIFIED_BUN_FLOOR) {
|
|
72
|
+
const parse = (v) => {
|
|
73
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
74
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
|
|
75
|
+
};
|
|
76
|
+
const a = parse(version);
|
|
77
|
+
const b = parse(floor);
|
|
78
|
+
if (!a || !b)
|
|
79
|
+
return undefined;
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
if (a[i] !== b[i])
|
|
82
|
+
return a[i] > b[i];
|
|
83
|
+
}
|
|
84
|
+
return true; // equal versions meet the floor
|
|
85
|
+
}
|
|
54
86
|
/**
|
|
55
87
|
* Synthesize the TLS-fingerprint status from three inputs. All three are
|
|
56
88
|
* passed explicitly so tests can cover every combination without touching
|
|
@@ -65,6 +97,21 @@ export function classifyRuntimeFingerprint(runningUnderBun, availableBunVersion,
|
|
|
65
97
|
// is readable; we don't require a separate probe. The caller passes the
|
|
66
98
|
// resolved version string as `availableBunVersion` in the bun case.
|
|
67
99
|
const bunVer = availableBunVersion ?? 'unknown';
|
|
100
|
+
// Being on Bun is necessary but NOT sufficient: only Bun ≥ the JA3-verified
|
|
101
|
+
// floor is measured to reproduce CC's ClientHello (#813). A readable version
|
|
102
|
+
// below the floor is the false-green case — dario auto-relaunches into an
|
|
103
|
+
// old Bun on PATH and would otherwise report a match while emitting a
|
|
104
|
+
// divergent JA3. An unreadable version (rare; Bun almost always exposes
|
|
105
|
+
// .version) has nothing to check, so we leave it as a best-effort match.
|
|
106
|
+
if (bunVer !== 'unknown' && bunVersionMeetsJa3Floor(bunVer) === false) {
|
|
107
|
+
return {
|
|
108
|
+
status: 'bun-ja3-unverified',
|
|
109
|
+
runtime: 'bun',
|
|
110
|
+
runtimeVersion: bunVer,
|
|
111
|
+
detail: `Bun v${bunVer} — under Bun, but its TLS ClientHello (JA3) is not verified to match Claude Code (known-good ≥ v${JA3_VERIFIED_BUN_FLOOR})`,
|
|
112
|
+
hint: `Upgrade Bun to ≥ v${JA3_VERIFIED_BUN_FLOOR} (https://bun.sh); older Bun ships an older BoringSSL whose ClientHello diverges from Claude Code's.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
68
115
|
return {
|
|
69
116
|
status: 'bun-match',
|
|
70
117
|
runtime: 'bun',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.12",
|
|
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": {
|