cc-haha/src/utils/claudeCodeCch.ts
2026-05-19 22:35:05 +08:00

227 lines
6.6 KiB
TypeScript

/**
* Claude Code CCH signing logic based on sub2api.
* Reference: https://github.com/Wei-Shaw/sub2api
* License: LGPL-3.0-or-later, Copyright (c) 2026 Wesley Liddick.
*/
import { CLAUDE_CODE_BILLING_HEADER_PREFIX } from '../constants/claudeCodeCompatibility.js'
const CCH_PLACEHOLDER = 'cch=00000;'
const CCH_PLACEHOLDER_RE = /\bcch=00000;/g
const CCH_SEED = 0x6E52736AC806831En
const MASK_64 = 0xffffffffffffffffn
const PRIME64_1 = 11400714785074694791n
const PRIME64_2 = 14029467366897019727n
const PRIME64_3 = 1609587929392839161n
const PRIME64_4 = 9650029242287828579n
const PRIME64_5 = 2870177450012600261n
const encoder = new TextEncoder()
const decoder = new TextDecoder()
type BillingFieldSelector = (parsed: unknown) => string[]
export function signClaudeCodeCCHInString(body: string): string {
return signClaudeCodeCCHInStringWithSelector(body, selectAnthropicBillingFields)
}
export function signClaudeCodeCCHInTransformedString(body: string): string {
return signClaudeCodeCCHInStringWithSelector(body, selectTransformedBillingFields)
}
function signClaudeCodeCCHInStringWithSelector(
body: string,
selectBillingFields: BillingFieldSelector,
): string {
if (!body.includes(CLAUDE_CODE_BILLING_HEADER_PREFIX) || !body.includes(CCH_PLACEHOLDER)) return body
let parsed: unknown
try {
parsed = JSON.parse(body) as unknown
} catch {
return body
}
if (countCCHPlaceholders(body) !== 1) return body
const billingFields = selectBillingFields(parsed)
const placeholderCount = billingFields.reduce((count, value) => count + countCCHPlaceholders(value), 0)
if (placeholderCount !== 1) return body
const billingField = billingFields.find(value => countCCHPlaceholders(value) === 1)
if (!billingField) return body
const fieldLiteral = JSON.stringify(billingField)
if (countLiteralOccurrences(body, fieldLiteral) !== 1) return body
const cch = computeClaudeCodeCCH(body)
const signedField = billingField.replace(CCH_PLACEHOLDER_RE, `cch=${cch};`)
return body.replace(fieldLiteral, JSON.stringify(signedField))
}
export function signClaudeCodeCCHBody(body: BodyInit | null | undefined): BodyInit | null | undefined {
if (typeof body === 'string') {
return signClaudeCodeCCHInString(body)
}
if (body instanceof Uint8Array) {
const raw = decoder.decode(body)
const signed = signClaudeCodeCCHInString(raw)
return signed === raw ? body : encoder.encode(signed)
}
return body
}
function selectAnthropicBillingFields(parsed: unknown): string[] {
if (!isRecord(parsed)) return []
const { system } = parsed
if (typeof system === 'string') {
return isBillingHeader(system) ? [system] : []
}
if (!Array.isArray(system)) return []
return system.flatMap(block => (
isTextBlock(block) && isBillingHeader(block.text) ? [block.text] : []
))
}
function selectTransformedBillingFields(parsed: unknown): string[] {
if (!isRecord(parsed)) return []
const fields: string[] = []
if (typeof parsed.instructions === 'string' && isBillingHeader(parsed.instructions)) {
fields.push(parsed.instructions)
}
if (Array.isArray(parsed.messages)) {
for (const message of parsed.messages) {
if (!isRecord(message)) continue
if (message.role === 'system' && typeof message.content === 'string' && isBillingHeader(message.content)) {
fields.push(message.content)
}
}
}
return fields
}
function isBillingHeader(value: string): boolean {
return value.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX)
}
function isTextBlock(value: unknown): value is { type: 'text'; text: string } {
return isRecord(value) && value.type === 'text' && typeof value.text === 'string'
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
function countCCHPlaceholders(value: string): number {
return value.match(CCH_PLACEHOLDER_RE)?.length ?? 0
}
function countLiteralOccurrences(body: string, literal: string): number {
let count = 0
let index = body.indexOf(literal)
while (index !== -1) {
count += 1
index = body.indexOf(literal, index + literal.length)
}
return count
}
function computeClaudeCodeCCH(body: string): string {
return (xxHash64Seeded(encoder.encode(body), CCH_SEED) & 0xfffffn)
.toString(16)
.padStart(5, '0')
}
export function xxHash64Seeded(data: Uint8Array, seed: bigint): bigint {
let offset = 0
let h64: bigint
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
if (data.length >= 32) {
const limit = data.length - 32
let v1 = (seed + PRIME64_1 + PRIME64_2) & MASK_64
let v2 = (seed + PRIME64_2) & MASK_64
let v3 = seed & MASK_64
let v4 = (seed - PRIME64_1) & MASK_64
while (offset <= limit) {
v1 = xxh64Round(v1, readU64(view, offset))
offset += 8
v2 = xxh64Round(v2, readU64(view, offset))
offset += 8
v3 = xxh64Round(v3, readU64(view, offset))
offset += 8
v4 = xxh64Round(v4, readU64(view, offset))
offset += 8
}
h64 = (
rotl64(v1, 1n) +
rotl64(v2, 7n) +
rotl64(v3, 12n) +
rotl64(v4, 18n)
) & MASK_64
h64 = xxh64MergeRound(h64, v1)
h64 = xxh64MergeRound(h64, v2)
h64 = xxh64MergeRound(h64, v3)
h64 = xxh64MergeRound(h64, v4)
} else {
h64 = (seed + PRIME64_5) & MASK_64
}
h64 = (h64 + BigInt(data.length)) & MASK_64
while (offset + 8 <= data.length) {
const k1 = xxh64Round(0n, readU64(view, offset))
h64 ^= k1
h64 = (rotl64(h64, 27n) * PRIME64_1 + PRIME64_4) & MASK_64
offset += 8
}
if (offset + 4 <= data.length) {
h64 ^= (BigInt(readU32(view, offset)) * PRIME64_1) & MASK_64
h64 = (rotl64(h64, 23n) * PRIME64_2 + PRIME64_3) & MASK_64
offset += 4
}
while (offset < data.length) {
h64 ^= (BigInt(data[offset]!) * PRIME64_5) & MASK_64
h64 = (rotl64(h64, 11n) * PRIME64_1) & MASK_64
offset += 1
}
h64 ^= h64 >> 33n
h64 = (h64 * PRIME64_2) & MASK_64
h64 ^= h64 >> 29n
h64 = (h64 * PRIME64_3) & MASK_64
h64 ^= h64 >> 32n
return h64 & MASK_64
}
function xxh64Round(acc: bigint, input: bigint): bigint {
return (rotl64((acc + input * PRIME64_2) & MASK_64, 31n) * PRIME64_1) & MASK_64
}
function xxh64MergeRound(acc: bigint, value: bigint): bigint {
acc ^= xxh64Round(0n, value)
return (acc * PRIME64_1 + PRIME64_4) & MASK_64
}
function rotl64(value: bigint, bits: bigint): bigint {
return ((value << bits) | (value >> (64n - bits))) & MASK_64
}
function readU64(view: DataView, offset: number): bigint {
return view.getBigUint64(offset, true)
}
function readU32(view: DataView, offset: number): number {
return view.getUint32(offset, true)
}