@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 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.177";
285
+ readonly maxTested: "2.1.178";
286
286
  };
287
287
  /**
288
288
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -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.177',
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. NOTE: real Claude Code does NOT randomise this — it's a
48
- // deterministic xxHash64 integrity hash over the serialized request body,
49
- // written in place over a `cch=00000` placeholder inside the billing-tag
50
- // system block (reverse-engineered on cc_version=2.1.37; see dario#528).
51
- //
52
- // We emit a random 5-hex value on purpose. The published seed
53
- // (0x6E52736AC806831E) + scope do NOT reproduce the cch on current CC
54
- // (falsified against a live 2.1.177 capture), and the seed/scope appear to
55
- // rotate between releases. A deterministic-but-wrong value is no better than
56
- // random against a server that recomputes — and a cleaner tell — whereas a
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
- finalBody = Buffer.from(JSON.stringify(r));
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.76",
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": [