Add constants and X-Msh-* header builder with persistent ~/.kimi/device_id

This commit is contained in:
lemon07r
2026-04-16 23:24:46 -04:00
parent e39f1bc5e5
commit 4df98757e3
2 changed files with 72 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
// Values mirror kimi-cli v1.35.0 1:1. When upstream bumps, update here and
// nothing else in the codebase should hard-code these strings.
//
// Source of truth: research/kimi-cli/src/kimi_cli/constant.py,
// research/kimi-cli/src/kimi_cli/auth/oauth.py
//
// NOTE: client_id is a public constant shipped inside the official CLI, not a
// secret. scope `kimi-code` is what routes the issued JWT to K2.6.
export const KIMI_CLI_VERSION = "1.35.0"
export const USER_AGENT = `KimiCodeCLI/${KIMI_CLI_VERSION}`
export const OAUTH_HOST = "https://auth.kimi.com"
export const OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`
export const OAUTH_TOKEN_URL = `${OAUTH_HOST}/api/oauth/token`
export const OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
export const OAUTH_SCOPE = "kimi-code"
export const OAUTH_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
export const OAUTH_REFRESH_GRANT = "refresh_token"
export const API_BASE_URL = "https://api.kimi.com/coding/v1"
export const MODEL_ID = "kimi-for-coding"
// Refresh a bit before the server-reported expiry so we never race it.
export const REFRESH_SAFETY_WINDOW_MS = 60_000
+47
View File
@@ -0,0 +1,47 @@
import os from "node:os"
import fs from "node:fs"
import path from "node:path"
import crypto from "node:crypto"
import { KIMI_CLI_VERSION, USER_AGENT } from "./constants.ts"
// kimi-cli persists its device id at `~/.kimi/device_id` as a plain UUIDv4
// hex string (no dashes). We intentionally share the same path so users who
// also run the real kimi CLI keep a single stable fingerprint. See
// research/kimi-cli/src/kimi_cli/auth/oauth.py (get_device_id).
const DEVICE_ID_DIR = path.join(os.homedir(), ".kimi")
const DEVICE_ID_PATH = path.join(DEVICE_ID_DIR, "device_id")
function ensureDir(dir: string) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
}
export function getDeviceId(): string {
ensureDir(DEVICE_ID_DIR)
if (fs.existsSync(DEVICE_ID_PATH)) {
const existing = fs.readFileSync(DEVICE_ID_PATH, "utf8").trim()
if (existing) return existing
}
const id = crypto.randomUUID().replace(/-/g, "")
fs.writeFileSync(DEVICE_ID_PATH, id, { mode: 0o600 })
return id
}
// Non-ASCII characters in HTTP headers will be rejected by Node's undici
// fetch (`TypeError: Invalid character in header content`). kimi-cli does the
// same sanitization in oauth._ascii_header_value.
function ascii(value: string): string {
return value.replace(/[^\x20-\x7e]/g, "?")
}
/** Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request. */
export function kimiHeaders(): Record<string, string> {
return {
"User-Agent": USER_AGENT,
"X-Msh-Platform": "kimi_cli",
"X-Msh-Version": KIMI_CLI_VERSION,
"X-Msh-Device-Name": ascii(os.hostname() || "unknown"),
"X-Msh-Device-Model": ascii(os.machine?.() || os.arch()),
"X-Msh-Device-Id": getDeviceId(),
"X-Msh-Os-Version": ascii(`${os.type()} ${os.release()}`),
}
}