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()),
    },
  }),
};