Spaces:
Runtime error
Runtime error
File size: 4,316 Bytes
cd8bd0a | 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 | import { KIMI_CODING_CONFIG } from "../constants/oauth";
import { randomUUID } from "crypto";
import fs from "fs";
import { arch, hostname, release, type as osType, version as osVersion } from "os";
import path from "path";
import { resolveDataDir } from "../../dataPaths";
const PLATFORM = "kimi_cli";
const VERSION = process.env.KIMI_CLI_VERSION || "1.36.0";
const DEVICE_ID_FILE = "kimi-coding-device-id";
function sanitizeHeaderValue(value, fallback = "unknown") {
const text = String(value ?? "").trim();
if (!text) return fallback;
return text.replace(/[^\x20-\x7e]/g, "").trim() || fallback;
}
function getDeviceModel() {
return [osType() || process.platform, release(), arch()].filter(Boolean).join(" ");
}
function generateDeviceId() {
return randomUUID().replace(/-/g, "");
}
function getKimiDeviceId() {
const configured = process.env.KIMI_CODING_DEVICE_ID?.trim();
if (configured) return configured;
try {
const oauthDir = path.join(resolveDataDir(), "oauth");
const devicePath = path.join(oauthDir, DEVICE_ID_FILE);
if (fs.existsSync(devicePath)) {
const existing = fs.readFileSync(devicePath, "utf8").trim();
if (existing) return existing;
}
fs.mkdirSync(oauthDir, { recursive: true });
const deviceId = generateDeviceId();
fs.writeFileSync(devicePath, deviceId, { encoding: "utf8", mode: 0o600 });
try {
fs.chmodSync(devicePath, 0o600);
} catch {}
return deviceId;
} catch {
return generateDeviceId();
}
}
// Custom headers required by Kimi OAuth
function getKimiOAuthHeaders() {
return {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"X-Msh-Platform": PLATFORM,
"X-Msh-Version": VERSION,
"X-Msh-Device-Name": sanitizeHeaderValue(hostname()),
"X-Msh-Device-Model": sanitizeHeaderValue(getDeviceModel()),
"X-Msh-Os-Version": sanitizeHeaderValue(osVersion()),
"X-Msh-Device-Id": sanitizeHeaderValue(getKimiDeviceId()),
};
}
export const kimiCoding = {
config: KIMI_CODING_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers: getKimiOAuthHeaders(),
body: new URLSearchParams({
client_id: config.clientId,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Device code request failed: ${error}`);
}
const data = await response.json();
const verificationUri = data.verification_uri || "https://www.kimi.com/code/authorize_device";
return {
device_code: data.device_code,
user_code: data.user_code,
verification_uri: verificationUri,
verification_uri_complete: data.verification_uri_complete || verificationUri,
expires_in: data.expires_in,
interval: data.interval || 5,
};
},
pollToken: async (config, deviceCode) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: getKimiOAuthHeaders(),
body: new URLSearchParams({
client_id: config.clientId,
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
});
let data;
try {
data = await response.json();
} catch (e) {
const text = await response.text();
data = { error: "invalid_response", error_description: text };
}
return {
ok: response.ok,
data: data,
};
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
tokenType: tokens.token_type,
scope: tokens.scope,
// Persist the device identity at login so refreshes use the SAME deviceId
// the device-code grant was issued against. Without this, tokenRefresh.ts
// falls back to pbkdf2(refresh_token, ...) — and Kimi rotates refresh_tokens
// per refresh, so the derived id changes every cycle and the anti-bot
// pipeline treats each refresh as a new device.
providerSpecificData: {
deviceId: getKimiDeviceId(),
deviceName: sanitizeHeaderValue(hostname()),
deviceModel: sanitizeHeaderValue(getDeviceModel()),
osVersion: sanitizeHeaderValue(osVersion()),
},
}),
};
|