@askalf/dario 4.8.76 → 4.8.77
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/cch.d.ts +18 -0
- package/dist/cch.js +139 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.js +26 -14
- package/package.json +2 -1
package/dist/cch.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Verified per-release seeds, keyed on `major.minor.patch`. */
|
|
2
|
+
export declare const CCH_SEEDS: Record<string, bigint>;
|
|
3
|
+
/** Canonical xxHash64 of `data` with a 64-bit `seed`. */
|
|
4
|
+
export declare function xxh64(data: Uint8Array, seed: bigint): bigint;
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic 5-hex cch for a serialized body under an EXPLICIT seed, or
|
|
7
|
+
* null if the body carries no `cch=XXXXX` token. Used by `scripts/cch-
|
|
8
|
+
* calibrate.mjs` to test candidate seeds against a live capture without going
|
|
9
|
+
* through the version table.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cchWithSeed(bodyText: string, seed: bigint): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic 5-hex `cch` for a serialized request body, or null when:
|
|
14
|
+
* - `version` (major.minor.patch) has no known seed, or
|
|
15
|
+
* - the body carries no `cch=XXXXX` billing token to stamp.
|
|
16
|
+
* A null return means the caller should keep its random placeholder.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cchForBody(bodyText: string, version: string): string | null;
|
package/dist/cch.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Deterministic Claude Code request-integrity hash (`cch`) — dario#528.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code stamps each /v1/messages request with a 5-hex `cch` token inside
|
|
4
|
+
// the `x-anthropic-billing-header` system block. It is NOT random: it is an
|
|
5
|
+
// xxHash64 over a canonical PROJECTION of the request body, masked to 20 bits.
|
|
6
|
+
// Reverse-engineered by lwsh123k (dario#528) and verified here against live
|
|
7
|
+
// captures on Claude Code 2.1.177:
|
|
8
|
+
//
|
|
9
|
+
// material = the serialized body, transformed by:
|
|
10
|
+
// 1. reset the `cch=XXXXX` token to `cch=00000`
|
|
11
|
+
// 2. blank the "model" VALUE (keep the key)
|
|
12
|
+
// 3. delete "fallbacks", "fallback_credit_token", "max_tokens"
|
|
13
|
+
// 4. re-serialize as compact JSON (JSON.stringify)
|
|
14
|
+
// cch = ( xxHash64(material, SEED[version]) & 0xFFFFF ) as zero-padded 5-hex
|
|
15
|
+
//
|
|
16
|
+
// The excluded fields are exactly the ones a routing proxy rewrites (model,
|
|
17
|
+
// fallbacks, max_tokens), so the value is STABLE across dario's rewrites — the
|
|
18
|
+
// upstream re-derives the same projection from whatever we send and recomputes
|
|
19
|
+
// the identical hash. dario therefore only has to hash its own final body.
|
|
20
|
+
//
|
|
21
|
+
// The seed rotates per Claude Code release and is keyed on major.minor.patch
|
|
22
|
+
// (the build-tag suffix, e.g. ".e2d" vs ".dd9", does NOT change it — verified
|
|
23
|
+
// against two captures with different suffixes, same 2.1.177 seed). An unknown
|
|
24
|
+
// version returns null so the caller falls back to a random value (the
|
|
25
|
+
// pre-dario#528 behavior) rather than emitting a confident-but-wrong hash that
|
|
26
|
+
// a validating server could single out.
|
|
27
|
+
/** Verified per-release seeds, keyed on `major.minor.patch`. */
|
|
28
|
+
export const CCH_SEEDS = {
|
|
29
|
+
'2.1.177': 0x4d659218e32a3268n,
|
|
30
|
+
};
|
|
31
|
+
const MASK = 0xfffffn;
|
|
32
|
+
const U64 = (1n << 64n) - 1n;
|
|
33
|
+
// xxHash64 primes.
|
|
34
|
+
const P1 = 0x9e3779b185ebca87n;
|
|
35
|
+
const P2 = 0xc2b2ae3d27d4eb4fn;
|
|
36
|
+
const P3 = 0x165667b19e3779f9n;
|
|
37
|
+
const P4 = 0x85ebca77c2b2ae63n;
|
|
38
|
+
const P5 = 0x27d4eb2f165667c5n;
|
|
39
|
+
const rotl = (x, r) => ((x << r) | (x >> (64n - r))) & U64;
|
|
40
|
+
function round(acc, input) {
|
|
41
|
+
acc = (acc + input * P2) & U64;
|
|
42
|
+
acc = rotl(acc, 31n);
|
|
43
|
+
return (acc * P1) & U64;
|
|
44
|
+
}
|
|
45
|
+
function mergeRound(acc, val) {
|
|
46
|
+
const r = round(0n, val);
|
|
47
|
+
acc = (acc ^ r) & U64;
|
|
48
|
+
return (acc * P1 + P4) & U64;
|
|
49
|
+
}
|
|
50
|
+
/** Canonical xxHash64 of `data` with a 64-bit `seed`. */
|
|
51
|
+
export function xxh64(data, seed) {
|
|
52
|
+
const len = data.length;
|
|
53
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
54
|
+
let h;
|
|
55
|
+
let i = 0;
|
|
56
|
+
if (len >= 32) {
|
|
57
|
+
let v1 = (seed + P1 + P2) & U64;
|
|
58
|
+
let v2 = (seed + P2) & U64;
|
|
59
|
+
let v3 = seed & U64;
|
|
60
|
+
let v4 = (seed - P1) & U64;
|
|
61
|
+
const limit = len - 32;
|
|
62
|
+
while (i <= limit) {
|
|
63
|
+
v1 = round(v1, dv.getBigUint64(i, true));
|
|
64
|
+
i += 8;
|
|
65
|
+
v2 = round(v2, dv.getBigUint64(i, true));
|
|
66
|
+
i += 8;
|
|
67
|
+
v3 = round(v3, dv.getBigUint64(i, true));
|
|
68
|
+
i += 8;
|
|
69
|
+
v4 = round(v4, dv.getBigUint64(i, true));
|
|
70
|
+
i += 8;
|
|
71
|
+
}
|
|
72
|
+
h = (rotl(v1, 1n) + rotl(v2, 7n) + rotl(v3, 12n) + rotl(v4, 18n)) & U64;
|
|
73
|
+
h = mergeRound(h, v1);
|
|
74
|
+
h = mergeRound(h, v2);
|
|
75
|
+
h = mergeRound(h, v3);
|
|
76
|
+
h = mergeRound(h, v4);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
h = (seed + P5) & U64;
|
|
80
|
+
}
|
|
81
|
+
h = (h + BigInt(len)) & U64;
|
|
82
|
+
while (i + 8 <= len) {
|
|
83
|
+
const k1 = round(0n, dv.getBigUint64(i, true));
|
|
84
|
+
h = (h ^ k1) & U64;
|
|
85
|
+
h = (rotl(h, 27n) * P1 + P4) & U64;
|
|
86
|
+
i += 8;
|
|
87
|
+
}
|
|
88
|
+
if (i + 4 <= len) {
|
|
89
|
+
h = (h ^ ((BigInt(dv.getUint32(i, true)) * P1) & U64)) & U64;
|
|
90
|
+
h = (rotl(h, 23n) * P2 + P3) & U64;
|
|
91
|
+
i += 4;
|
|
92
|
+
}
|
|
93
|
+
while (i < len) {
|
|
94
|
+
h = (h ^ ((BigInt(data[i]) * P5) & U64)) & U64;
|
|
95
|
+
h = (rotl(h, 11n) * P1) & U64;
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
h = (h ^ (h >> 33n)) & U64;
|
|
99
|
+
h = (h * P2) & U64;
|
|
100
|
+
h = (h ^ (h >> 29n)) & U64;
|
|
101
|
+
h = (h * P3) & U64;
|
|
102
|
+
h = (h ^ (h >> 32n)) & U64;
|
|
103
|
+
return h;
|
|
104
|
+
}
|
|
105
|
+
const CCH_RE = /cch=[0-9a-fA-F]{5}/;
|
|
106
|
+
/** Build the canonical cch pre-image bytes from a serialized request body. */
|
|
107
|
+
function cchMaterial(bodyText) {
|
|
108
|
+
const zeroed = bodyText.replace(CCH_RE, 'cch=00000'); // first occurrence only
|
|
109
|
+
const body = JSON.parse(zeroed);
|
|
110
|
+
body.model = '';
|
|
111
|
+
delete body.fallbacks;
|
|
112
|
+
delete body.fallback_credit_token;
|
|
113
|
+
delete body.max_tokens;
|
|
114
|
+
return new TextEncoder().encode(JSON.stringify(body));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic 5-hex cch for a serialized body under an EXPLICIT seed, or
|
|
118
|
+
* null if the body carries no `cch=XXXXX` token. Used by `scripts/cch-
|
|
119
|
+
* calibrate.mjs` to test candidate seeds against a live capture without going
|
|
120
|
+
* through the version table.
|
|
121
|
+
*/
|
|
122
|
+
export function cchWithSeed(bodyText, seed) {
|
|
123
|
+
if (!CCH_RE.test(bodyText))
|
|
124
|
+
return null;
|
|
125
|
+
const h = xxh64(cchMaterial(bodyText), seed) & MASK;
|
|
126
|
+
return h.toString(16).padStart(5, '0');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Deterministic 5-hex `cch` for a serialized request body, or null when:
|
|
130
|
+
* - `version` (major.minor.patch) has no known seed, or
|
|
131
|
+
* - the body carries no `cch=XXXXX` billing token to stamp.
|
|
132
|
+
* A null return means the caller should keep its random placeholder.
|
|
133
|
+
*/
|
|
134
|
+
export function cchForBody(bodyText, version) {
|
|
135
|
+
const seed = CCH_SEEDS[version];
|
|
136
|
+
if (seed === undefined)
|
|
137
|
+
return null;
|
|
138
|
+
return cchWithSeed(bodyText, seed);
|
|
139
|
+
}
|
|
@@ -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.178";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -786,7 +786,7 @@ export function _resetInstalledVersionProbeForTest() {
|
|
|
786
786
|
*/
|
|
787
787
|
export const SUPPORTED_CC_RANGE = {
|
|
788
788
|
min: '1.0.0',
|
|
789
|
-
maxTested: '2.1.
|
|
789
|
+
maxTested: '2.1.178',
|
|
790
790
|
};
|
|
791
791
|
/**
|
|
792
792
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/proxy.js
CHANGED
|
@@ -9,6 +9,7 @@ import { arch, platform } from 'node:process';
|
|
|
9
9
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
10
10
|
import { buildHealthResponse } from './health-response.js';
|
|
11
11
|
import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, CC_TEMPLATE } from './cc-template.js';
|
|
12
|
+
import { cchForBody } from './cch.js';
|
|
12
13
|
import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
|
|
13
14
|
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs } from './pool.js';
|
|
14
15
|
import { Analytics, billingBucketFromClaim } from './analytics.js';
|
|
@@ -44,19 +45,16 @@ function computeBuildTag(userMessage, version) {
|
|
|
44
45
|
const chars = [4, 7, 20].map(i => userMessage[i] || '0').join('');
|
|
45
46
|
return createHash('sha256').update(`${BILLING_SEED}${chars}${version}`).digest('hex').slice(0, 3);
|
|
46
47
|
}
|
|
47
|
-
// Per-request cch.
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// random value keeps the right shape (5 lowercase hex) and varies per request
|
|
58
|
-
// like the real one. Revisit only if Anthropic starts rejecting on cch, which
|
|
59
|
-
// would require re-RE'ing the current binary for the live seed + pre-image.
|
|
48
|
+
// Per-request cch PLACEHOLDER. Claude Code's cch is a deterministic xxHash64
|
|
49
|
+
// over a projection of the request body (see src/cch.ts, dario#528). We can
|
|
50
|
+
// only compute the real value once the final body is assembled, so we seed the
|
|
51
|
+
// billing tag with a random 5-hex token here and overwrite it in place with the
|
|
52
|
+
// deterministic value at serialize time (the `cchForBody` call near the
|
|
53
|
+
// JSON.stringify of the outbound body). For Claude Code versions whose seed we
|
|
54
|
+
// haven't reverse-engineered, `cchForBody` returns null and this random value
|
|
55
|
+
// is what ships — same behavior as before dario#528, and a better tell than a
|
|
56
|
+
// confident-but-wrong deterministic hash. Operators can force the random path
|
|
57
|
+
// on every request with DARIO_CCH=random.
|
|
60
58
|
function computeCch() {
|
|
61
59
|
return randomBytes(3).toString('hex').slice(0, 5);
|
|
62
60
|
}
|
|
@@ -1768,7 +1766,21 @@ export async function startProxy(opts = {}) {
|
|
|
1768
1766
|
oc.effort = bestSupportedEffort(supportedEfforts);
|
|
1769
1767
|
}
|
|
1770
1768
|
}
|
|
1771
|
-
|
|
1769
|
+
// dario#528: overwrite the random cch placeholder with the real
|
|
1770
|
+
// deterministic value for Claude Code versions whose seed we've
|
|
1771
|
+
// reverse-engineered. cchForBody hashes a projection of THIS final
|
|
1772
|
+
// body, so it must run after every mutation above. It returns null
|
|
1773
|
+
// for unknown versions (keep the random placeholder) or bodies with
|
|
1774
|
+
// no billing token. Only the template-replay path is stamped —
|
|
1775
|
+
// passthrough / count_tokens forward the client's body (and its own
|
|
1776
|
+
// cch) verbatim. Reversible kill-switch: DARIO_CCH=random.
|
|
1777
|
+
let outboundText = JSON.stringify(r);
|
|
1778
|
+
if (!passthrough && !isCountTokens && process.env.DARIO_CCH !== 'random') {
|
|
1779
|
+
const realCch = cchForBody(outboundText, cliVersion);
|
|
1780
|
+
if (realCch)
|
|
1781
|
+
outboundText = outboundText.replace(/cch=[0-9a-fA-F]{5}/, `cch=${realCch}`);
|
|
1782
|
+
}
|
|
1783
|
+
finalBody = Buffer.from(outboundText);
|
|
1772
1784
|
}
|
|
1773
1785
|
catch { /* not JSON, send as-is */ }
|
|
1774
1786
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.77",
|
|
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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"compat": "node test/compat.mjs",
|
|
33
33
|
"lint:pkg": "node scripts/check-package-json.mjs",
|
|
34
34
|
"drift:sdk": "node scripts/check-sdk-drift.mjs",
|
|
35
|
+
"cch:calibrate": "node scripts/cch-calibrate.mjs",
|
|
35
36
|
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|