codex-copilot-dx 0.4.14 → 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 +14 -1
- package/package.json +1 -1
- package/src/auth.mjs +300 -13
- package/src/copilot.mjs +41 -11
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,6 +48,15 @@ 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
|
+
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:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
rm ~/.local/share/copilot-api/github_token
|
|
57
|
+
codex-copilot-dx
|
|
58
|
+
```
|
|
59
|
+
|
|
51
60
|
### Claude App opt-in
|
|
52
61
|
|
|
53
62
|
Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
|
|
@@ -83,6 +92,10 @@ Environment variables:
|
|
|
83
92
|
| `CCDX_CONFIGURE_CLAUDE_DESKTOP` | unset | Set to `1` to write the Claude App 3P gateway profile during startup |
|
|
84
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 |
|
|
85
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 |
|
|
86
99
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
|
|
87
100
|
| `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
|
|
88
101
|
|
package/package.json
CHANGED
package/src/auth.mjs
CHANGED
|
@@ -6,11 +6,23 @@ 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");
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
export function githubReauthMessage(reason, home = os.homedir()) {
|
|
19
|
+
const tokenPath = githubTokenPath(home);
|
|
20
|
+
return `${reason}
|
|
21
|
+
Delete the saved GitHub token, then run codex-copilot-dx again to log in:
|
|
22
|
+
rm '${tokenPath}'
|
|
23
|
+
codex-copilot-dx`;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
// Map GitHub polling responses to a small local state machine.
|
|
15
27
|
export function interpretPoll(data) {
|
|
16
28
|
if (typeof data.access_token === "string" && data.access_token) return { state: "done", token: data.access_token };
|
|
@@ -23,6 +35,263 @@ export function interpretPoll(data) {
|
|
|
23
35
|
|
|
24
36
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
25
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
|
+
|
|
26
295
|
// On macOS, copy the user code and open the verification page. Fail quietly.
|
|
27
296
|
function openAndCopy(userCode, verificationUri) {
|
|
28
297
|
if (process.platform !== "darwin") return;
|
|
@@ -42,32 +311,48 @@ function openAndCopy(userCode, verificationUri) {
|
|
|
42
311
|
} catch {}
|
|
43
312
|
}
|
|
44
313
|
|
|
45
|
-
export async function ensureAuth(
|
|
46
|
-
|
|
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);
|
|
47
323
|
if (fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
48
|
-
|
|
49
|
-
|
|
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"));
|
|
50
330
|
}
|
|
51
|
-
|
|
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..."));
|
|
52
336
|
|
|
53
337
|
// Request a device code.
|
|
54
|
-
const codeResp = await
|
|
338
|
+
const codeResp = await fetchImpl("https://github.com/login/device/code", {
|
|
55
339
|
method: "POST",
|
|
56
340
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
57
341
|
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
342
|
+
signal,
|
|
58
343
|
});
|
|
59
344
|
if (!codeResp.ok) throw new Error(`device code request failed: ${codeResp.status}`);
|
|
60
345
|
const { device_code, user_code, verification_uri, interval } = await codeResp.json();
|
|
61
346
|
|
|
62
347
|
// Prompt the user.
|
|
63
|
-
|
|
64
|
-
|
|
348
|
+
log(`\n${status("info", `Open ${verification_uri}`)}\n${status("info", `Enter code: ${user_code}`)}\n`);
|
|
349
|
+
openAndCopyFn(user_code, verification_uri);
|
|
65
350
|
|
|
66
351
|
// Poll until GitHub completes the device flow.
|
|
67
352
|
let waitMs = (interval || 5) * 1000;
|
|
68
353
|
while (true) {
|
|
69
354
|
await sleep(waitMs);
|
|
70
|
-
const pollResp = await
|
|
355
|
+
const pollResp = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
71
356
|
method: "POST",
|
|
72
357
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
73
358
|
body: JSON.stringify({
|
|
@@ -75,6 +360,7 @@ export async function ensureAuth() {
|
|
|
75
360
|
device_code,
|
|
76
361
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
77
362
|
}),
|
|
363
|
+
signal,
|
|
78
364
|
});
|
|
79
365
|
if (!pollResp.ok) {
|
|
80
366
|
// Treat transient network/server errors as pending and retry.
|
|
@@ -83,8 +369,8 @@ export async function ensureAuth() {
|
|
|
83
369
|
const data = await pollResp.json();
|
|
84
370
|
const r = interpretPoll(data);
|
|
85
371
|
if (r.state === "done") {
|
|
86
|
-
writeToken(r.token);
|
|
87
|
-
|
|
372
|
+
writeToken(r.token, home);
|
|
373
|
+
log(status("ok", "Login successful"));
|
|
88
374
|
return;
|
|
89
375
|
}
|
|
90
376
|
if (r.state === "slow") { waitMs += 5000; continue; }
|
|
@@ -93,8 +379,9 @@ export async function ensureAuth() {
|
|
|
93
379
|
}
|
|
94
380
|
}
|
|
95
381
|
|
|
96
|
-
function writeToken(token) {
|
|
97
|
-
const GITHUB_TOKEN_PATH = githubTokenPath();
|
|
382
|
+
export function writeToken(token, home = os.homedir()) {
|
|
383
|
+
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
98
384
|
fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
|
|
99
385
|
fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
|
|
386
|
+
fs.chmodSync(GITHUB_TOKEN_PATH, 0o600);
|
|
100
387
|
}
|
package/src/copilot.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import { randomUUID } from "node:crypto";
|
|
5
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
6
6
|
|
|
7
|
-
const GITHUB_TOKEN_PATH = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
|
|
8
7
|
export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
|
|
9
8
|
let apiBase = DEFAULT_API_BASE;
|
|
10
9
|
const IMG_MAX_DIM = parseInt(process.env.CCDX_IMG_MAX_DIM || "2048", 10);
|
|
@@ -248,22 +247,24 @@ export async function refreshVSCodeVersion() {
|
|
|
248
247
|
let copilotToken = null;
|
|
249
248
|
let copilotTokenExpiry = 0;
|
|
250
249
|
|
|
251
|
-
function getGithubToken() {
|
|
250
|
+
function getGithubToken({ home = os.homedir() } = {}) {
|
|
251
|
+
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
252
252
|
if (!fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
253
|
-
throw new Error("GitHub token not found. Run
|
|
253
|
+
throw new Error("GitHub token not found. Run codex-copilot-dx again to log in.");
|
|
254
254
|
}
|
|
255
|
-
|
|
255
|
+
const token = fs.readFileSync(GITHUB_TOKEN_PATH, "utf-8").trim();
|
|
256
|
+
if (!token) throw new Error(githubReauthMessage("GitHub token file is empty."));
|
|
257
|
+
return token;
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const ghToken = getGithubToken();
|
|
261
|
-
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`, {
|
|
262
262
|
headers: { Authorization: `token ${ghToken}`, Accept: "application/json" },
|
|
263
263
|
signal,
|
|
264
264
|
});
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cacheCopilotTokenData(data) {
|
|
267
268
|
if (!data.token) throw new Error("Copilot token response missing token field");
|
|
268
269
|
copilotToken = data.token;
|
|
269
270
|
apiBase = parseApiBase(data);
|
|
@@ -274,6 +275,35 @@ export async function getCopilotToken({ signal } = {}) {
|
|
|
274
275
|
return copilotToken;
|
|
275
276
|
}
|
|
276
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
|
+
|
|
277
307
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
278
308
|
export async function chatCompletions(chatReq, { signal } = {}) {
|
|
279
309
|
const token = await getCopilotToken({ signal });
|