Download packages/oauth/src/identity.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 7.61 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/oauth/src/identity.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/oauth/src/identity.ts
-
curl -L -o identity.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/oauth/src/identity.ts
7.61 kB
| /** | |
| * Kimi host and device identity header factories. | |
| * | |
| * The caller owns the host identity (product name + host app version) | |
| * and the `homeDir` where the stable device id is stored. This module | |
| * intentionally keeps no global CLI version or environment-derived | |
| * production state. | |
| */ | |
| import { execFileSync } from 'node:child_process'; | |
| import { randomUUID } from 'node:crypto'; | |
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | |
| import { arch, hostname, release, type } from 'node:os'; | |
| import { join } from 'node:path'; | |
| import type { DeviceHeaders } from './types'; | |
| export const KIMI_CODE_PLATFORM = 'kimi_code_cli'; | |
| export interface KimiHostIdentity { | |
| readonly productName: string; | |
| readonly version: string; | |
| /** | |
| * `X-Msh-Platform` value reported to the OAuth host and managed endpoints | |
| * (e.g. `kimi_code_cli`, `kimi_code_desktop`). Every host must state its own | |
| * explicitly — `KIMI_CODE_PLATFORM` is the CLI's value, not a default to | |
| * inherit silently. | |
| */ | |
| readonly platform: string; | |
| readonly userAgentSuffix?: string | undefined; | |
| } | |
| export interface KimiIdentityOptions extends KimiHostIdentity { | |
| readonly homeDir: string; | |
| } | |
| export interface CreateKimiDeviceIdOptions { | |
| /** Invoked synchronously the first time a device id is minted on this machine. */ | |
| readonly onFirstLaunch?: ((id: string) => void) | undefined; | |
| } | |
| export function readKimiDeviceId(homeDir: string): string | null { | |
| const deviceIdPath = join(homeDir, 'device_id'); | |
| if (!existsSync(deviceIdPath)) return null; | |
| try { | |
| const text = readFileSync(deviceIdPath, 'utf-8').trim(); | |
| return text.length > 0 ? text : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function createKimiDeviceId( | |
| homeDir: string, | |
| options: CreateKimiDeviceIdOptions = {}, | |
| ): string { | |
| const existing = readKimiDeviceId(homeDir); | |
| if (existing !== null) return existing; | |
| const id = randomUUID(); | |
| try { | |
| mkdirSync(homeDir, { recursive: true, mode: 0o700 }); | |
| writeFileSync(join(homeDir, 'device_id'), id, { encoding: 'utf-8', mode: 0o600 }); | |
| } catch { | |
| // Best-effort: requests can still use the in-memory id. | |
| } | |
| if (options.onFirstLaunch !== undefined) { | |
| try { | |
| options.onFirstLaunch(id); | |
| } catch { | |
| // Telemetry callback must not affect device id creation. | |
| } | |
| } | |
| return id; | |
| } | |
| export function createKimiDeviceHeaders(options: { | |
| readonly homeDir: string; | |
| readonly version: string; | |
| /** Required and validated like the version: non-empty ASCII, no fallback — | |
| a blank or fabricated platform would silently misreport the host. */ | |
| readonly platform: string; | |
| }): DeviceHeaders { | |
| return { | |
| 'X-Msh-Platform': requiredAsciiHeader(options.platform, 'Kimi identity platform'), | |
| 'X-Msh-Version': requiredAsciiHeader(options.version, 'Kimi identity version'), | |
| 'X-Msh-Device-Name': asciiHeader(hostname()), | |
| 'X-Msh-Device-Model': asciiHeader(deviceModel()), | |
| 'X-Msh-Os-Version': asciiHeader(release()), | |
| 'X-Msh-Device-Id': createKimiDeviceId(options.homeDir), | |
| }; | |
| } | |
| export function createKimiUserAgent(options: { | |
| readonly productName: string; | |
| readonly version: string; | |
| readonly userAgentSuffix?: string | undefined; | |
| }): string { | |
| const product = requiredAsciiHeader(options.productName, 'Kimi identity product'); | |
| const version = requiredAsciiHeader(options.version, 'Kimi identity version'); | |
| const suffix = | |
| options.userAgentSuffix === undefined ? undefined : asciiHeader(options.userAgentSuffix, ''); | |
| return suffix === undefined || suffix.length === 0 | |
| ? `${product}/${version}` | |
| : `${product}/${version} (${suffix})`; | |
| } | |
| /** | |
| * Swap the product token of a User-Agent produced by | |
| * {@link createKimiUserAgent}, keeping the version and optional suffix intact | |
| * (`kimi-code-cli/1.2.3 (web)` → `acme/1.2.3 (web)`). | |
| * | |
| * Lives next to the builder on purpose: the format knowledge — product token, | |
| * `/`, version, parenthesized suffix — must exist in exactly one place, so a | |
| * change to the builder cannot silently desynchronize the rewriter. Callers | |
| * pass an already-normalized ASCII token; a blank or non-ASCII product still | |
| * throws rather than emitting an invalid header. | |
| * | |
| * A value that does not carry a `/` is treated as a bare product token and | |
| * replaced wholesale. | |
| */ | |
| export function replaceUserAgentProduct(userAgent: string, product: string): string { | |
| const cleaned = requiredAsciiHeader(product, 'Kimi identity product'); | |
| const separator = userAgent.indexOf('/'); | |
| return separator < 0 ? cleaned : `${cleaned}${userAgent.slice(separator)}`; | |
| } | |
| export function createKimiDefaultHeaders(options: KimiIdentityOptions): Record<string, string> { | |
| return { | |
| 'User-Agent': createKimiUserAgent(options), | |
| ...createKimiDeviceHeaders({ | |
| homeDir: options.homeDir, | |
| version: options.version, | |
| platform: options.platform, | |
| }), | |
| }; | |
| } | |
| /** | |
| * Env var carrying extra headers applied to every outbound provider request | |
| * (LLM chat and `/models` listing). Mirrors `ANTHROPIC_CUSTOM_HEADERS`: | |
| * newline-separated `Name: Value` lines; lines without a colon are skipped; | |
| * names and values are trimmed. | |
| * | |
| * These headers form the lowest-precedence layer — the Kimi identity headers | |
| * (User-Agent, X-Msh-*), per-provider `customHeaders`, and request auth | |
| * (Authorization) all override them. | |
| * | |
| * Unlike the device identity headers above, this is intentionally | |
| * environment-derived and stateless (re-read on every call) so callers can | |
| * apply it uniformly without plumbing the value through every host layer. | |
| */ | |
| export const KIMI_CODE_CUSTOM_HEADERS_ENV = 'KIMI_CODE_CUSTOM_HEADERS'; | |
| export function parseKimiCodeCustomHeaders( | |
| env: NodeJS.ProcessEnv = process.env, | |
| ): Record<string, string> { | |
| const raw = env[KIMI_CODE_CUSTOM_HEADERS_ENV]?.trim(); | |
| if (raw === undefined || raw.length === 0) return {}; | |
| const headers: Record<string, string> = {}; | |
| for (const line of raw.split('\n')) { | |
| const colon = line.indexOf(':'); | |
| if (colon < 0) continue; | |
| const name = line.slice(0, colon).trim(); | |
| if (name.length === 0) continue; | |
| headers[name] = line.slice(colon + 1).trim(); | |
| } | |
| return headers; | |
| } | |
| export function assertKimiHostIdentity(identity: KimiHostIdentity | undefined): KimiHostIdentity { | |
| if (identity === undefined) { | |
| throw new Error('Kimi host identity is required. Pass the host product name and version.'); | |
| } | |
| requiredAsciiHeader(identity.productName, 'Kimi identity product'); | |
| requiredAsciiHeader(identity.version, 'Kimi identity version'); | |
| return identity; | |
| } | |
| function deviceModel(): string { | |
| const os = type(); | |
| const version = release(); | |
| const osArch = arch(); | |
| if (os === 'Darwin') return `macOS ${macOsProductVersion() ?? version} ${osArch}`; | |
| if (os === 'Windows_NT') return `Windows ${version} ${osArch}`; | |
| return `${os} ${version} ${osArch}`.trim(); | |
| } | |
| function macOsProductVersion(): string | undefined { | |
| try { | |
| const version = execFileSync('/usr/bin/sw_vers', ['-productVersion'], { | |
| encoding: 'utf-8', | |
| timeout: 1000, | |
| }).trim(); | |
| return version.length > 0 ? version : undefined; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| function asciiHeader(value: string, fallback = 'unknown'): string { | |
| const cleaned = value.replaceAll(/[^\u0020-\u007E]/g, '').trim(); | |
| return cleaned.length > 0 ? cleaned : fallback; | |
| } | |
| function requiredAsciiHeader(value: string, fieldName: string): string { | |
| const cleaned = asciiHeader(value, ''); | |
| if (cleaned.length === 0) { | |
| throw new Error(`${fieldName} must be a non-empty ASCII string.`); | |
| } | |
| return cleaned; | |
| } | |