@askalf/dario 4.8.76 → 4.8.78
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 +26 -0
- package/dist/cch.js +161 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.js +25 -14
- package/package.json +2 -1
package/dist/cch.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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;
|
|
19
|
+
/**
|
|
20
|
+
* Return `bodyText` with the billing-tag cch replaced in place by the
|
|
21
|
+
* deterministic value for `version`, or unchanged when the version has no
|
|
22
|
+
* known seed or the body has no billing token. The replacement is anchored to
|
|
23
|
+
* the billing tag (CCH_RE), so conversation content that quotes a cch is never
|
|
24
|
+
* touched. Used by the proxy at outbound-serialize time.
|
|
25
|
+
*/
|
|
26
|
+
export declare function stampCch(bodyText: string, version: string): string;
|
package/dist/cch.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
// Match the cch token INSIDE the billing tag specifically — never a stray
|
|
106
|
+
// `cch=#####` quoted in conversation content (which sorts before `system` in
|
|
107
|
+
// the body, so a naive first-match would grab it, mis-hash, AND silently
|
|
108
|
+
// rewrite the user's text at stamp time — dario#528). Anchor on the
|
|
109
|
+
// `cc_entrypoint=<value>; cch=` that immediately precedes it in the billing
|
|
110
|
+
// header. The entrypoint value is BOUNDED ({1,32}) so the match stays linear
|
|
111
|
+
// on a 10 MB body — an unbounded `[^"]*?` span here is O(n^2) when the anchor
|
|
112
|
+
// repeats (CodeQL js/polynomial-redos). Anchoring is also what real CC must
|
|
113
|
+
// do, so this matches upstream behavior, not just our own correctness.
|
|
114
|
+
const CCH_RE = /(cc_entrypoint=[a-z0-9-]{1,32}; cch=)[0-9a-fA-F]{5}(?=;)/;
|
|
115
|
+
/** Build the canonical cch pre-image bytes from a serialized request body. */
|
|
116
|
+
function cchMaterial(bodyText) {
|
|
117
|
+
const zeroed = bodyText.replace(CCH_RE, (_m, prefix) => `${prefix}00000`);
|
|
118
|
+
const body = JSON.parse(zeroed);
|
|
119
|
+
body.model = '';
|
|
120
|
+
delete body.fallbacks;
|
|
121
|
+
delete body.fallback_credit_token;
|
|
122
|
+
delete body.max_tokens;
|
|
123
|
+
return new TextEncoder().encode(JSON.stringify(body));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Deterministic 5-hex cch for a serialized body under an EXPLICIT seed, or
|
|
127
|
+
* null if the body carries no `cch=XXXXX` token. Used by `scripts/cch-
|
|
128
|
+
* calibrate.mjs` to test candidate seeds against a live capture without going
|
|
129
|
+
* through the version table.
|
|
130
|
+
*/
|
|
131
|
+
export function cchWithSeed(bodyText, seed) {
|
|
132
|
+
if (!CCH_RE.test(bodyText))
|
|
133
|
+
return null;
|
|
134
|
+
const h = xxh64(cchMaterial(bodyText), seed) & MASK;
|
|
135
|
+
return h.toString(16).padStart(5, '0');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Deterministic 5-hex `cch` for a serialized request body, or null when:
|
|
139
|
+
* - `version` (major.minor.patch) has no known seed, or
|
|
140
|
+
* - the body carries no `cch=XXXXX` billing token to stamp.
|
|
141
|
+
* A null return means the caller should keep its random placeholder.
|
|
142
|
+
*/
|
|
143
|
+
export function cchForBody(bodyText, version) {
|
|
144
|
+
const seed = CCH_SEEDS[version];
|
|
145
|
+
if (seed === undefined)
|
|
146
|
+
return null;
|
|
147
|
+
return cchWithSeed(bodyText, seed);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Return `bodyText` with the billing-tag cch replaced in place by the
|
|
151
|
+
* deterministic value for `version`, or unchanged when the version has no
|
|
152
|
+
* known seed or the body has no billing token. The replacement is anchored to
|
|
153
|
+
* the billing tag (CCH_RE), so conversation content that quotes a cch is never
|
|
154
|
+
* touched. Used by the proxy at outbound-serialize time.
|
|
155
|
+
*/
|
|
156
|
+
export function stampCch(bodyText, version) {
|
|
157
|
+
const cch = cchForBody(bodyText, version);
|
|
158
|
+
if (cch === null)
|
|
159
|
+
return bodyText;
|
|
160
|
+
return bodyText.replace(CCH_RE, (_m, prefix) => `${prefix}${cch}`);
|
|
161
|
+
}
|
|
@@ -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 { stampCch } 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,20 @@ 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. stampCch hashes a projection of THIS final body
|
|
1772
|
+
// (so it must run after every mutation above) and replaces the cch
|
|
1773
|
+
// anchored to the billing tag — never a cch quoted in conversation
|
|
1774
|
+
// content. No-op for unknown versions (keep the random placeholder).
|
|
1775
|
+
// Only the template-replay path is stamped — passthrough /
|
|
1776
|
+
// count_tokens forward the client's body (and its own cch) verbatim.
|
|
1777
|
+
// Reversible kill-switch: DARIO_CCH=random.
|
|
1778
|
+
let outboundText = JSON.stringify(r);
|
|
1779
|
+
if (!passthrough && !isCountTokens && process.env.DARIO_CCH !== 'random') {
|
|
1780
|
+
outboundText = stampCch(outboundText, cliVersion);
|
|
1781
|
+
}
|
|
1782
|
+
finalBody = Buffer.from(outboundText);
|
|
1772
1783
|
}
|
|
1773
1784
|
catch { /* not JSON, send as-is */ }
|
|
1774
1785
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.78",
|
|
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": [
|