codex-copilot-dx 0.4.15 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/auth.mjs +292 -13
- package/src/copilot.mjs +38 -14
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ npx codex-copilot-dx@latest
|
|
|
27
27
|
> Tip: the `@latest` suffix forces `npx` to fetch the newest release instead of using a stale cached copy.
|
|
28
28
|
|
|
29
29
|
On first run, it will:
|
|
30
|
-
1. Authenticate with GitHub via device flow (if needed)
|
|
30
|
+
1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
|
|
31
31
|
2. Print the local package version and warn when a newer npm release is available
|
|
32
32
|
3. Start the adapter on loopback (`127.0.0.1:2026`)
|
|
33
33
|
4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
|
|
@@ -48,7 +48,9 @@ codex-copilot-dx doctor
|
|
|
48
48
|
|
|
49
49
|
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude App gateway profile, and whether the local adapter port is listening.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
When the saved token is missing, `codex-copilot-dx` first looks for compatible local Copilot GitHub tokens, validates them with GitHub and Copilot, and imports a valid one before starting device login. It checks explicit token sources (`CCDX_GITHUB_TOKEN`, `CCDX_GITHUB_TOKEN_PATH`, `CCDX_GITHUB_TOKEN_PATHS`) plus common local `auth.json` layouts under application config directories. It does not rely on a specific app name. If a saved token later fails with `401` or `403`, the adapter repeats that discovery and retries once before asking you to reauthenticate.
|
|
52
|
+
|
|
53
|
+
If Copilot token refresh still fails with `401` or `403`, the saved GitHub token may be expired, revoked, or missing Copilot access. Delete the saved token and start the tool again to trigger GitHub device login:
|
|
52
54
|
|
|
53
55
|
```bash
|
|
54
56
|
rm ~/.local/share/copilot-api/github_token
|
|
@@ -90,6 +92,10 @@ Environment variables:
|
|
|
90
92
|
| `CCDX_CONFIGURE_CLAUDE_DESKTOP` | unset | Set to `1` to write the Claude App 3P gateway profile during startup |
|
|
91
93
|
| `CCDX_CLAUDE_DESKTOP_API_KEY` | generated for opt-in setup | Bearer key written into the Claude App profile and recognized by the adapter for model discovery |
|
|
92
94
|
| `CCDX_CLAUDE_MODEL_ALIASES` | built-in Claude aliases | Comma-separated Desktop-to-upstream aliases, for example `claude-sonnet-4-6=claude-sonnet-4.6` |
|
|
95
|
+
| `CCDX_GITHUB_TOKEN` | unset | Explicit GitHub Copilot OAuth token to validate and import before device login |
|
|
96
|
+
| `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token to validate and import before device login |
|
|
97
|
+
| `CCDX_GITHUB_TOKEN_PATHS` | unset | Multiple token files separated by the platform path delimiter (`:` on macOS/Linux, `;` on Windows) |
|
|
98
|
+
| `CCDX_DISABLE_TOKEN_DISCOVERY` | unset | Set to `1` to skip local token discovery and go straight to the saved token or device flow |
|
|
93
99
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
|
|
94
100
|
| `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
|
|
95
101
|
|
package/package.json
CHANGED
package/src/auth.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import { status } from "./status.mjs";
|
|
|
6
6
|
|
|
7
7
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // Public GitHub Copilot client ID.
|
|
8
8
|
const SCOPE = "read:user";
|
|
9
|
+
const GITHUB_API = "https://api.github.com";
|
|
10
|
+
const COPILOT_TOKEN_URL = `${GITHUB_API}/copilot_internal/v2/token`;
|
|
11
|
+
const DISABLE_TOKEN_DISCOVERY_VALUES = new Set(["1", "true", "yes"]);
|
|
12
|
+
const MAX_AUTH_JSON_BYTES = 1024 * 1024;
|
|
9
13
|
|
|
10
14
|
export function githubTokenPath(home = os.homedir()) {
|
|
11
15
|
return path.join(home, ".local", "share", "copilot-api", "github_token");
|
|
@@ -31,6 +35,263 @@ export function interpretPoll(data) {
|
|
|
31
35
|
|
|
32
36
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
33
37
|
|
|
38
|
+
function expandHome(filePath, home = os.homedir()) {
|
|
39
|
+
if (!filePath) return "";
|
|
40
|
+
if (filePath === "~") return home;
|
|
41
|
+
if (filePath.startsWith("~/")) return path.join(home, filePath.slice(2));
|
|
42
|
+
return filePath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTokenDiscoveryDisabled(env = process.env) {
|
|
46
|
+
return DISABLE_TOKEN_DISCOVERY_VALUES.has(String(env.CCDX_DISABLE_TOKEN_DISCOVERY || "").toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function splitPathList(value) {
|
|
50
|
+
return String(value || "")
|
|
51
|
+
.split(path.delimiter)
|
|
52
|
+
.map((item) => item.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function localAuthRoots(home, env = process.env) {
|
|
57
|
+
const roots = [
|
|
58
|
+
path.join(home, "Library", "Application Support"),
|
|
59
|
+
env.APPDATA ? expandHome(env.APPDATA, home) : "",
|
|
60
|
+
env.XDG_CONFIG_HOME ? expandHome(env.XDG_CONFIG_HOME, home) : path.join(home, ".config"),
|
|
61
|
+
].filter(Boolean);
|
|
62
|
+
return [...new Set(roots)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addAuthJsonSource(sources, seen, filePath) {
|
|
66
|
+
if (seen.has(filePath)) return;
|
|
67
|
+
if (!fs.existsSync(filePath)) return;
|
|
68
|
+
seen.add(filePath);
|
|
69
|
+
sources.push({ type: "auth-json", path: filePath });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function localAuthJsonSources(home, env = process.env) {
|
|
73
|
+
const sources = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
|
|
76
|
+
for (const root of localAuthRoots(home, env)) {
|
|
77
|
+
let appDirs;
|
|
78
|
+
try {
|
|
79
|
+
appDirs = fs.readdirSync(root, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const appDirent of appDirs) {
|
|
85
|
+
if (!appDirent.isDirectory()) continue;
|
|
86
|
+
const appDir = path.join(root, appDirent.name);
|
|
87
|
+
addAuthJsonSource(sources, seen, path.join(appDir, "auth.json"));
|
|
88
|
+
|
|
89
|
+
const profilesDir = path.join(appDir, "profiles");
|
|
90
|
+
let profileDirs;
|
|
91
|
+
try {
|
|
92
|
+
profileDirs = fs.readdirSync(profilesDir, { withFileTypes: true });
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const profileDirent of profileDirs) {
|
|
98
|
+
if (!profileDirent.isDirectory()) continue;
|
|
99
|
+
addAuthJsonSource(sources, seen, path.join(profilesDir, profileDirent.name, "auth.json"));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sources;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function explicitTokenFileSources(home, env = process.env) {
|
|
108
|
+
const paths = [];
|
|
109
|
+
if (typeof env.CCDX_GITHUB_TOKEN_PATH === "string" && env.CCDX_GITHUB_TOKEN_PATH.trim()) {
|
|
110
|
+
paths.push(env.CCDX_GITHUB_TOKEN_PATH.trim());
|
|
111
|
+
}
|
|
112
|
+
paths.push(...splitPathList(env.CCDX_GITHUB_TOKEN_PATHS));
|
|
113
|
+
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
return paths
|
|
116
|
+
.map((filePath) => expandHome(filePath, home))
|
|
117
|
+
.filter((filePath) => {
|
|
118
|
+
if (seen.has(filePath)) return false;
|
|
119
|
+
seen.add(filePath);
|
|
120
|
+
return true;
|
|
121
|
+
})
|
|
122
|
+
.map((filePath) => ({
|
|
123
|
+
type: "token-file",
|
|
124
|
+
name: "configured token file",
|
|
125
|
+
path: filePath,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function looksLikeCopilotAuthJson(json) {
|
|
130
|
+
if (!json || typeof json !== "object") return false;
|
|
131
|
+
if (json.ghcAuth || json.gitHubTokens || json.githubToken || json.githubCopilot || json.copilot) return true;
|
|
132
|
+
return typeof json.access_token === "string" && Boolean(json.copilotAccess || json.copilotToken || json.copilot_token);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readSmallJson(filePath) {
|
|
136
|
+
const stat = fs.statSync(filePath);
|
|
137
|
+
if (!stat.isFile() || stat.size > MAX_AUTH_JSON_BYTES) return null;
|
|
138
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function githubTokenSources({
|
|
142
|
+
home = os.homedir(),
|
|
143
|
+
env = process.env,
|
|
144
|
+
} = {}) {
|
|
145
|
+
if (isTokenDiscoveryDisabled(env)) return [];
|
|
146
|
+
const sources = [];
|
|
147
|
+
if (typeof env.CCDX_GITHUB_TOKEN === "string" && env.CCDX_GITHUB_TOKEN.trim()) {
|
|
148
|
+
sources.push({ type: "env", name: "CCDX_GITHUB_TOKEN", token: env.CCDX_GITHUB_TOKEN.trim() });
|
|
149
|
+
}
|
|
150
|
+
sources.push(...explicitTokenFileSources(home, env));
|
|
151
|
+
sources.push(...localAuthJsonSources(home, env));
|
|
152
|
+
return sources;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function sourceDescription(source) {
|
|
156
|
+
if (!source) return "unknown source";
|
|
157
|
+
if (source.type === "env") return source.name;
|
|
158
|
+
if (source.type === "token-file") return `${source.name} (${source.path})`;
|
|
159
|
+
if (source.type === "auth-json") return `local auth file (${source.path})`;
|
|
160
|
+
return source.path || source.name || "unknown source";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function extractGithubTokenFromAuthJson(json) {
|
|
164
|
+
const candidates = [
|
|
165
|
+
json?.ghcAuth?.gitHubTokens?.access_token,
|
|
166
|
+
json?.ghcAuth?.gitHubTokens?.accessToken,
|
|
167
|
+
json?.ghcAuth?.githubToken,
|
|
168
|
+
json?.gitHubTokens?.access_token,
|
|
169
|
+
json?.gitHubTokens?.accessToken,
|
|
170
|
+
json?.githubToken,
|
|
171
|
+
json?.access_token,
|
|
172
|
+
json?.accessToken,
|
|
173
|
+
];
|
|
174
|
+
return candidates.find((value) => typeof value === "string" && value.trim())?.trim() || "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function readGithubTokenSource(source) {
|
|
178
|
+
if (source?.type === "env") return source.token || "";
|
|
179
|
+
if (source?.type === "token-file") return fs.readFileSync(source.path, "utf8").trim();
|
|
180
|
+
if (source?.type === "auth-json") {
|
|
181
|
+
const json = readSmallJson(source.path);
|
|
182
|
+
if (!looksLikeCopilotAuthJson(json)) return "";
|
|
183
|
+
return extractGithubTokenFromAuthJson(json);
|
|
184
|
+
}
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function validateGithubToken(token, {
|
|
189
|
+
fetchImpl = fetch,
|
|
190
|
+
signal,
|
|
191
|
+
} = {}) {
|
|
192
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
193
|
+
return { ok: false, reason: "empty_token" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const headers = { Authorization: `token ${token.trim()}`, Accept: "application/json" };
|
|
197
|
+
let userResp;
|
|
198
|
+
try {
|
|
199
|
+
userResp = await fetchImpl(`${GITHUB_API}/user`, { headers, signal });
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return { ok: false, transient: true, reason: "github_user_request_failed", error: e };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!userResp.ok) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
status: userResp.status,
|
|
208
|
+
reason: userResp.status === 401 || userResp.status === 403 ? "github_token_invalid" : "github_user_failed",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let userData = {};
|
|
213
|
+
try {
|
|
214
|
+
userData = await userResp.json();
|
|
215
|
+
} catch {}
|
|
216
|
+
const login = typeof userData.login === "string" ? userData.login : "";
|
|
217
|
+
|
|
218
|
+
let copilotResp;
|
|
219
|
+
try {
|
|
220
|
+
copilotResp = await fetchImpl(COPILOT_TOKEN_URL, { headers, signal });
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { ok: false, transient: true, reason: "copilot_token_request_failed", login, error: e };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!copilotResp.ok) {
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
status: copilotResp.status,
|
|
229
|
+
reason: copilotResp.status === 401 || copilotResp.status === 403 ? "copilot_access_denied" : "copilot_token_failed",
|
|
230
|
+
login,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let copilotTokenData = {};
|
|
235
|
+
try {
|
|
236
|
+
copilotTokenData = await copilotResp.json();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return { ok: false, reason: "copilot_token_parse_failed", login, error: e };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!copilotTokenData.token) {
|
|
242
|
+
return { ok: false, reason: "copilot_token_missing", login };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { ok: true, login, copilotTokenData };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function discoverGithubToken({
|
|
249
|
+
home = os.homedir(),
|
|
250
|
+
env = process.env,
|
|
251
|
+
fetchImpl = fetch,
|
|
252
|
+
signal,
|
|
253
|
+
excludeTokens = [],
|
|
254
|
+
} = {}) {
|
|
255
|
+
const excluded = new Set(excludeTokens);
|
|
256
|
+
const seen = new Set(excluded);
|
|
257
|
+
const failures = [];
|
|
258
|
+
|
|
259
|
+
for (const source of githubTokenSources({ home, env })) {
|
|
260
|
+
let token = "";
|
|
261
|
+
try {
|
|
262
|
+
token = readGithubTokenSource(source).trim();
|
|
263
|
+
} catch (e) {
|
|
264
|
+
failures.push({ source, reason: "read_failed", error: e });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!token || seen.has(token)) continue;
|
|
269
|
+
seen.add(token);
|
|
270
|
+
|
|
271
|
+
const validation = await validateGithubToken(token, { fetchImpl, signal });
|
|
272
|
+
if (validation.ok) return { ok: true, token, source, validation };
|
|
273
|
+
failures.push({ source, validation });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { ok: false, failures };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function importDiscoveredGithubToken({
|
|
280
|
+
home = os.homedir(),
|
|
281
|
+
env = process.env,
|
|
282
|
+
fetchImpl = fetch,
|
|
283
|
+
signal,
|
|
284
|
+
excludeTokens = [],
|
|
285
|
+
log = console.log,
|
|
286
|
+
} = {}) {
|
|
287
|
+
const discovered = await discoverGithubToken({ home, env, fetchImpl, signal, excludeTokens });
|
|
288
|
+
if (!discovered.ok) return null;
|
|
289
|
+
writeToken(discovered.token, home);
|
|
290
|
+
const login = discovered.validation.login ? ` for ${discovered.validation.login}` : "";
|
|
291
|
+
log(status("ok", `Imported GitHub token from ${sourceDescription(discovered.source)}${login}`));
|
|
292
|
+
return discovered;
|
|
293
|
+
}
|
|
294
|
+
|
|
34
295
|
// On macOS, copy the user code and open the verification page. Fail quietly.
|
|
35
296
|
function openAndCopy(userCode, verificationUri) {
|
|
36
297
|
if (process.platform !== "darwin") return;
|
|
@@ -50,32 +311,48 @@ function openAndCopy(userCode, verificationUri) {
|
|
|
50
311
|
} catch {}
|
|
51
312
|
}
|
|
52
313
|
|
|
53
|
-
export async function ensureAuth(
|
|
54
|
-
|
|
314
|
+
export async function ensureAuth({
|
|
315
|
+
home = os.homedir(),
|
|
316
|
+
env = process.env,
|
|
317
|
+
fetchImpl = fetch,
|
|
318
|
+
signal,
|
|
319
|
+
log = console.log,
|
|
320
|
+
openAndCopyFn = openAndCopy,
|
|
321
|
+
} = {}) {
|
|
322
|
+
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
55
323
|
if (fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
56
|
-
|
|
57
|
-
|
|
324
|
+
const existing = fs.readFileSync(GITHUB_TOKEN_PATH, "utf8").trim();
|
|
325
|
+
if (existing) {
|
|
326
|
+
log(status("ok", "GitHub token found"));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
log(status("warn", "GitHub token file is empty"));
|
|
58
330
|
}
|
|
59
|
-
|
|
331
|
+
|
|
332
|
+
const imported = await importDiscoveredGithubToken({ home, env, fetchImpl, signal, log });
|
|
333
|
+
if (imported) return;
|
|
334
|
+
|
|
335
|
+
log(status("wait", "No usable GitHub token found. Starting device login..."));
|
|
60
336
|
|
|
61
337
|
// Request a device code.
|
|
62
|
-
const codeResp = await
|
|
338
|
+
const codeResp = await fetchImpl("https://github.com/login/device/code", {
|
|
63
339
|
method: "POST",
|
|
64
340
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
65
341
|
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
342
|
+
signal,
|
|
66
343
|
});
|
|
67
344
|
if (!codeResp.ok) throw new Error(`device code request failed: ${codeResp.status}`);
|
|
68
345
|
const { device_code, user_code, verification_uri, interval } = await codeResp.json();
|
|
69
346
|
|
|
70
347
|
// Prompt the user.
|
|
71
|
-
|
|
72
|
-
|
|
348
|
+
log(`\n${status("info", `Open ${verification_uri}`)}\n${status("info", `Enter code: ${user_code}`)}\n`);
|
|
349
|
+
openAndCopyFn(user_code, verification_uri);
|
|
73
350
|
|
|
74
351
|
// Poll until GitHub completes the device flow.
|
|
75
352
|
let waitMs = (interval || 5) * 1000;
|
|
76
353
|
while (true) {
|
|
77
354
|
await sleep(waitMs);
|
|
78
|
-
const pollResp = await
|
|
355
|
+
const pollResp = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
79
356
|
method: "POST",
|
|
80
357
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
81
358
|
body: JSON.stringify({
|
|
@@ -83,6 +360,7 @@ export async function ensureAuth() {
|
|
|
83
360
|
device_code,
|
|
84
361
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
85
362
|
}),
|
|
363
|
+
signal,
|
|
86
364
|
});
|
|
87
365
|
if (!pollResp.ok) {
|
|
88
366
|
// Treat transient network/server errors as pending and retry.
|
|
@@ -91,8 +369,8 @@ export async function ensureAuth() {
|
|
|
91
369
|
const data = await pollResp.json();
|
|
92
370
|
const r = interpretPoll(data);
|
|
93
371
|
if (r.state === "done") {
|
|
94
|
-
writeToken(r.token);
|
|
95
|
-
|
|
372
|
+
writeToken(r.token, home);
|
|
373
|
+
log(status("ok", "Login successful"));
|
|
96
374
|
return;
|
|
97
375
|
}
|
|
98
376
|
if (r.state === "slow") { waitMs += 5000; continue; }
|
|
@@ -101,8 +379,9 @@ export async function ensureAuth() {
|
|
|
101
379
|
}
|
|
102
380
|
}
|
|
103
381
|
|
|
104
|
-
function writeToken(token) {
|
|
105
|
-
const GITHUB_TOKEN_PATH = githubTokenPath();
|
|
382
|
+
export function writeToken(token, home = os.homedir()) {
|
|
383
|
+
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
106
384
|
fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
|
|
107
385
|
fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
|
|
386
|
+
fs.chmodSync(GITHUB_TOKEN_PATH, 0o600);
|
|
108
387
|
}
|
package/src/copilot.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { status } from "./status.mjs";
|
|
4
|
-
import { githubReauthMessage, githubTokenPath } from "./auth.mjs";
|
|
5
|
+
import { githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
|
|
7
8
|
let apiBase = DEFAULT_API_BASE;
|
|
@@ -246,8 +247,8 @@ export async function refreshVSCodeVersion() {
|
|
|
246
247
|
let copilotToken = null;
|
|
247
248
|
let copilotTokenExpiry = 0;
|
|
248
249
|
|
|
249
|
-
function getGithubToken() {
|
|
250
|
-
const GITHUB_TOKEN_PATH = githubTokenPath();
|
|
250
|
+
function getGithubToken({ home = os.homedir() } = {}) {
|
|
251
|
+
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
251
252
|
if (!fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
252
253
|
throw new Error("GitHub token not found. Run codex-copilot-dx again to log in.");
|
|
253
254
|
}
|
|
@@ -256,20 +257,14 @@ function getGithubToken() {
|
|
|
256
257
|
return token;
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const ghToken = getGithubToken();
|
|
262
|
-
const resp = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, {
|
|
260
|
+
function requestCopilotToken(ghToken, { fetchImpl = fetch, signal } = {}) {
|
|
261
|
+
return fetchImpl(`${GITHUB_API}/copilot_internal/v2/token`, {
|
|
263
262
|
headers: { Authorization: `token ${ghToken}`, Accept: "application/json" },
|
|
264
263
|
signal,
|
|
265
264
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
throw new Error(`Failed to get Copilot token: ${resp.status}`);
|
|
271
|
-
}
|
|
272
|
-
const data = await resp.json();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cacheCopilotTokenData(data) {
|
|
273
268
|
if (!data.token) throw new Error("Copilot token response missing token field");
|
|
274
269
|
copilotToken = data.token;
|
|
275
270
|
apiBase = parseApiBase(data);
|
|
@@ -280,6 +275,35 @@ export async function getCopilotToken({ signal } = {}) {
|
|
|
280
275
|
return copilotToken;
|
|
281
276
|
}
|
|
282
277
|
|
|
278
|
+
export async function getCopilotToken({
|
|
279
|
+
signal,
|
|
280
|
+
home = os.homedir(),
|
|
281
|
+
env = process.env,
|
|
282
|
+
fetchImpl = fetch,
|
|
283
|
+
} = {}) {
|
|
284
|
+
if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
|
|
285
|
+
const ghToken = getGithubToken({ home });
|
|
286
|
+
const resp = await requestCopilotToken(ghToken, { fetchImpl, signal });
|
|
287
|
+
if (!resp.ok) {
|
|
288
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
289
|
+
const imported = await importDiscoveredGithubToken({
|
|
290
|
+
home,
|
|
291
|
+
env,
|
|
292
|
+
fetchImpl,
|
|
293
|
+
signal,
|
|
294
|
+
excludeTokens: [ghToken],
|
|
295
|
+
});
|
|
296
|
+
if (imported?.validation?.copilotTokenData?.token) {
|
|
297
|
+
return cacheCopilotTokenData(imported.validation.copilotTokenData);
|
|
298
|
+
}
|
|
299
|
+
throw new Error(githubReauthMessage(`Failed to get Copilot token: ${resp.status}. The saved GitHub token may be expired, revoked, or missing Copilot access.`));
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`Failed to get Copilot token: ${resp.status}`);
|
|
302
|
+
}
|
|
303
|
+
const data = await resp.json();
|
|
304
|
+
return cacheCopilotTokenData(data);
|
|
305
|
+
}
|
|
306
|
+
|
|
283
307
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
284
308
|
export async function chatCompletions(chatReq, { signal } = {}) {
|
|
285
309
|
const token = await getCopilotToken({ signal });
|