File size: 4,002 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
import { CODEBUDDY_CN_CONFIG } from "../constants/oauth";

/**
 * CodeBuddy CN (Tencent — copilot.tencent.com) — custom device-auth flow.
 *
 *   1. POST stateUrl → { code: 0, data: { state, authUrl } }
 *   2. Open authUrl in the browser
 *   3. GET tokenUrl?state=<state> until { code: 0, data.accessToken } (11217 = pending)
 *
 * Mirrors the official CodeBuddy CLI's distinguishing detail: poll is GET with
 * the state as a query param, NOT POST/body.
 */
type CodeBuddyConfig = typeof CODEBUDDY_CN_CONFIG;

interface CodeBuddyDeviceCodeResponse {
  device_code: string;
  user_code: string;
  verification_uri: string;
  verification_uri_complete: string;
  expires_in: number;
  interval: number;
}

interface CodeBuddyTokens {
  access_token: string;
  refresh_token: string;
  token_type: string;
  expires_in?: number;
}

interface CodeBuddyPollResult {
  ok: boolean;
  data: Record<string, unknown> | CodeBuddyTokens;
}

export const codebuddyCn = {
  config: CODEBUDDY_CN_CONFIG,
  flowType: "device_code" as const,

  requestDeviceCode: async (config: CodeBuddyConfig): Promise<CodeBuddyDeviceCodeResponse> => {
    const response = await fetch(config.stateUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        "User-Agent": config.userAgent,
        "X-Requested-With": "XMLHttpRequest",
        "X-Domain": "copilot.tencent.com",
        "X-No-Authorization": "true",
        "X-No-User-Id": "true",
        "X-Product": "SaaS",
      },
      body: JSON.stringify({ platform: config.platform }),
    });

    if (!response.ok) {
      const err = await response.text();
      throw new Error(`CodeBuddy state request failed (${response.status})`);
    }

    const json = (await response.json()) as { code?: number; data?: any; msg?: string };
    if (json.code !== 0 || !json.data?.state) {
      throw new Error(`CodeBuddy state error: ${json.msg || "no state in response"}`);
    }

    const state = String(json.data.state);
    const authUrl = String(json.data.authUrl || json.data.url || "");
    return {
      device_code: state,
      user_code: state,
      verification_uri: authUrl,
      verification_uri_complete: authUrl,
      expires_in: 600,
      interval: Math.max(1, Math.floor((config.pollInterval || 5000) / 1000)),
    };
  },

  pollToken: async (config: CodeBuddyConfig, deviceCode: string): Promise<CodeBuddyPollResult> => {
    // GET with state as a query param (not POST/body) — matches the official CLI's
    // /v2/plugin/auth/token?state=... endpoint shape.
    const response = await fetch(
      `${config.tokenUrl}?state=${encodeURIComponent(deviceCode)}`,
      {
        method: "GET",
        headers: {
          Accept: "application/json",
          "User-Agent": config.userAgent,
          "X-Requested-With": "XMLHttpRequest",
          "X-Domain": "copilot.tencent.com",
          "X-No-Authorization": "true",
          "X-No-User-Id": "true",
          "X-No-Enterprise-Id": "true",
          "X-No-Department-Info": "true",
          "X-Product": "SaaS",
        },
      }
    );
    if (!response.ok) return { ok: false, data: { error: "request_failed" } };
    const data = (await response.json()) as { code?: number; data?: any; msg?: string };
    // code 11217 = pending (RetryFetchToken), code 0 = success
    if (data.code === 0 && data.data?.accessToken) {
      return {
        ok: true,
        data: {
          access_token: data.data.accessToken,
          refresh_token: data.data.refreshToken || "",
          token_type: data.data.tokenType || "Bearer",
          expires_in: data.data.expiresIn,
        },
      };
    }
    return { ok: false, data: { code: data.code, msg: data.msg } };
  },

  mapTokens: (tokens: CodeBuddyTokens) => ({
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresIn: tokens.expires_in || 86400,
    providerSpecificData: {},
  }),
};

export default codebuddyCn;