File size: 8,968 Bytes
67d18ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | /**
* Identity header builders β emit the ZCode desktop client's companion
* headers so the proxy is indistinguishable from the official client at the
* fingerprinting layer.
*
* TWO distinct bundle functions are mirrored (ZCode 3.11.2, `_reverse/zcode.cjs`):
*
* 1. `csn` = buildCliZCodeSourceHeaders (CLI LLM path, wrapped by `x4i`
* which appends `X-ZCode-Agent: "glm"` as the LAST header) β used for
* every LLM completion request β {@link buildLlmIdentityHeaders}.
* Shape: HTTP-Referer, User-Agent, [X-ZCode-App-Version], X-Title,
* X-Release-Channel (always), X-Client-Language (always, "unknown"
* fallback), X-Client-Timezone (always, "unknown" fallback),
* [X-Platform], X-Os-Category (when platform resolves), [X-Os-Version],
* X-ZCode-Agent ("glm", last). NO X-Device-Mid β the CLI LLM path never
* carried it.
*
* 2. `HRt` = buildZCodeSourceHeadersFromContext (host-side control-plane
* fetches: endpoint-routing configs, signing gate, claim/billing) β
* used by {@link buildIdentityHeaders}. Shape keeps the historical
* `pio`-derived order with conditional language/timezone and the
* optional X-Device-Mid (server-required on the claim/billing plane
* since the 0828 campaign). Consumers: endpoint-routing.ts,
* client-signing.ts, claim/client.ts, routes-quota.ts, async bridge.
*
* Both gate header values through the bundle's `fio` printable-ASCII rule;
* `n = fio(...)` validates appVersion and, when it fails, drops
* X-ZCode-App-Version entirely and falls the User-Agent back to
* `ZCode/unknown`.
*
* Runtime values are read via env overrides (matching the existing
* ZCODE_IDENTITY_PLATFORM/ARCH/RELEASE pattern) so the Android entry can emit
* desktop-Linux identity without changing this module:
* - ZCODE_IDENTITY_RELEASE_CHANNEL
* - ZCODE_IDENTITY_CLIENT_LANGUAGE (default: Intl locale, e.g. "zh-CN")
* - ZCODE_IDENTITY_CLIENT_TIMEZONE (default: Intl timezone, e.g. "Asia/Shanghai")
* - ZCODE_IDENTITY_DEVICE_MID (no default; omitted unless set)
*
* @see _reverse/NOTEPAD.md "How Credential is Used for LLM Calls"
*/
import os from "node:os";
import type { ProxyIdentity } from "../config/types.js";
/** Printable-ASCII gate copied from the ZCode bundle's `fio` helper. */
const ASCII_PRINTABLE = /^[\x20-\x7e]+$/;
/** Resolve the appVersion the way `fio` does: trimmed + printable ASCII, else undefined. */
function resolveAppVersion(raw: string | undefined): string | undefined {
if (typeof raw !== "string") return undefined;
const v = raw.trim();
return v.length > 0 && ASCII_PRINTABLE.test(v) ? v : undefined;
}
/** Normalize a header value: trimmed + printable ASCII, else undefined. */
export function normalizePrintableHeaderValue(raw: string | undefined): string | undefined {
if (typeof raw !== "string") return undefined;
const v = raw.trim();
return v.length > 0 && ASCII_PRINTABLE.test(v) ? v : undefined;
}
function normalizeOsCategory(platform: NodeJS.Platform): string {
switch (platform) {
case "darwin":
return "macos";
case "win32":
return "windows";
default:
return "linux";
}
}
/** Mirrors the bundle's `lsa()` / `V8i()`: Intl locale, wrapped in try/catch. */
function resolveClientLanguage(): string | undefined {
const override = normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_CLIENT_LANGUAGE);
if (override) return override;
try {
return Intl.DateTimeFormat().resolvedOptions().locale || undefined;
} catch {
return undefined;
}
}
/** Mirrors the bundle's `csa()`: Intl timezone, wrapped in try/catch. */
function resolveClientTimezone(): string | undefined {
const override = normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_CLIENT_TIMEZONE);
if (override) return override;
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
} catch {
return undefined;
}
}
interface ResolvedIdentityValues {
n?: string;
platform?: string;
platformForCategory: NodeJS.Platform;
arch?: string;
release?: string;
releaseChannel: string;
clientLanguage?: string;
clientTimezone?: string;
deviceMid?: string;
}
/** Shared env/config resolution for both builders (values only β ordering differs per builder). */
function resolveIdentityValues(id: ProxyIdentity): ResolvedIdentityValues {
// Env overrides (ZCODE_IDENTITY_PLATFORM/ARCH/RELEASE) let the Android entry
// emit desktop-Linux identity headers without changing this module.
return {
n: resolveAppVersion(id.appVersion),
platform: normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_PLATFORM ?? process.platform),
platformForCategory: (process.env.ZCODE_IDENTITY_PLATFORM ?? process.platform) as NodeJS.Platform,
arch: normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_ARCH ?? os.arch()),
release: normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_RELEASE ?? os.release()),
// bundle IL(): ZCODE_ENV==="test" ? "test" : "production" β always resolves.
// Mirror that default; ZCODE_IDENTITY_RELEASE_CHANNEL stays an explicit override.
releaseChannel: normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_RELEASE_CHANNEL)
?? (process.env.ZCODE_ENV?.trim().toLowerCase() === "test" ? "test" : "production"),
clientLanguage: resolveClientLanguage(),
clientTimezone: resolveClientTimezone(),
// env (Android NodeRunner injection) wins over the config.yaml value (desktop
// persistence) β both are UUIDv4 generated once and reused forever.
deviceMid: normalizePrintableHeaderValue(process.env.ZCODE_IDENTITY_DEVICE_MID)
?? normalizePrintableHeaderValue(id.deviceMid),
};
}
/**
* Identity headers for LLM completion requests β mirrors the bundle's CLI
* source-headers builder `csn` (buildCliZCodeSourceHeaders) + the `x4i`
* wrapper that appends `X-ZCode-Agent: "glm"` LAST. Differences vs the
* control-plane set (buildIdentityHeaders): language/timezone are ALWAYS
* emitted (falling back to "unknown"), X-Release-Channel sits right after
* X-Title, X-ZCode-Agent is the final header, and X-Device-Mid is NEVER sent.
* Pure function.
*/
export function buildLlmIdentityHeaders(id: ProxyIdentity): Record<string, string> {
const v = resolveIdentityValues(id);
return {
"HTTP-Referer": id.refererOrigin,
"User-Agent": `ZCode/${v.n ?? "unknown"}`,
...(v.n ? { "X-ZCode-App-Version": v.n } : {}),
"X-Title": `Z Code@${id.sourceTitle}`,
"X-Release-Channel": v.releaseChannel,
"X-Client-Language": v.clientLanguage ?? "unknown",
"X-Client-Timezone": v.clientTimezone ?? "unknown",
...(v.platform && v.arch ? { "X-Platform": `${v.platform}-${v.arch}` } : {}),
...(v.platform ? { "X-Os-Category": normalizeOsCategory(v.platformForCategory) } : {}),
...(v.release ? { "X-Os-Version": v.release } : {}),
"X-ZCode-Agent": "glm",
};
}
/**
* Control-plane identity headers β mirrors the host-side bundle builder
* `HRt` (buildZCodeSourceHeadersFromContext), reached us via the historical
* `pio` shape. Used by endpoint-routing, client-signing gate, claim/billing
* and the async bridge β NOT for LLM completion requests (use
* {@link buildLlmIdentityHeaders} there).
*
* Order (with X-ZCode-Agent kept between X-Title and X-Platform):
* HTTP-Referer, User-Agent, [X-ZCode-App-Version], X-Title, X-ZCode-Agent,
* [X-Platform], [X-Release-Channel], [X-Client-Language], [X-Client-Timezone],
* [X-Os-Category], [X-Os-Version], [X-Device-Mid]
*
* Returns `Record<string, string>` rather than a fixed interface because
* several headers are conditionally omitted.
*/
export function buildIdentityHeaders(id: ProxyIdentity): Record<string, string> {
const v = resolveIdentityValues(id);
return {
"HTTP-Referer": id.refererOrigin,
"User-Agent": `ZCode/${v.n ?? "unknown"}`,
...(v.n ? { "X-ZCode-App-Version": v.n } : {}),
"X-Title": `Z Code@${id.sourceTitle}`,
"X-ZCode-Agent": "glm",
...(v.platform && v.arch ? { "X-Platform": `${v.platform}-${v.arch}` } : {}),
...(v.releaseChannel ? { "X-Release-Channel": v.releaseChannel } : {}),
...(v.clientLanguage ? { "X-Client-Language": v.clientLanguage } : {}),
...(v.clientTimezone ? { "X-Client-Timezone": v.clientTimezone } : {}),
...(v.platform ? { "X-Os-Category": normalizeOsCategory(v.platformForCategory) } : {}),
...(v.release ? { "X-Os-Version": v.release } : {}),
...(v.deviceMid ? { "X-Device-Mid": v.deviceMid } : {}),
};
}
/**
* Cache key for process-wide singletons that embed a `ProxyIdentity`
* (endpoint routing, client signing): two configs producing the same key can
* share the same service instance.
*/
export function identityCacheKey(identity: ProxyIdentity): string {
return JSON.stringify([identity.appVersion, identity.sourceTitle, identity.refererOrigin, identity.deviceMid ?? ""]);
}
|