@nick3/copilot-api 1.10.29 → 1.10.34
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 +41 -9
- package/README.zh-CN.md +39 -7
- package/dist/admin/assets/index-Cl_ViIW_.js +110 -0
- package/dist/admin/index.html +1 -1
- package/dist/{auth-nO-eHeO_.js → auth-Cc11G9V9.js} +2 -2
- package/dist/{auth-nO-eHeO_.js.map → auth-Cc11G9V9.js.map} +1 -1
- package/dist/{check-usage-ZifYvA3w.js → check-usage-C2QE6R93.js} +2 -2
- package/dist/{check-usage-ZifYvA3w.js.map → check-usage-C2QE6R93.js.map} +1 -1
- package/dist/{config-CmhIPHn_.js → config-BaU_aWgi.js} +35 -4
- package/dist/config-BaU_aWgi.js.map +1 -0
- package/dist/{debug-DvpksqEL.js → debug-BKqoXB_p.js} +2 -2
- package/dist/{debug-DvpksqEL.js.map → debug-BKqoXB_p.js.map} +1 -1
- package/dist/main.js +4 -4
- package/dist/{responses-bridge-registry-BJ5Sbh6-.js → responses-bridge-registry-DqCoY6Ex.js} +14 -7
- package/dist/responses-bridge-registry-DqCoY6Ex.js.map +1 -0
- package/dist/{server-DJ3_UGc4.js → server-C7pCkArb.js} +636 -187
- package/dist/server-C7pCkArb.js.map +1 -0
- package/dist/{start-DaB0AcjZ.js → start-CdLbBkRA.js} +4 -4
- package/dist/{start-DaB0AcjZ.js.map → start-CdLbBkRA.js.map} +1 -1
- package/dist/token-671YFxgv.js +947 -0
- package/dist/token-671YFxgv.js.map +1 -0
- package/package.json +2 -2
- package/dist/admin/assets/index-BAh4eOwM.js +0 -110
- package/dist/config-CmhIPHn_.js.map +0 -1
- package/dist/responses-bridge-registry-BJ5Sbh6-.js.map +0 -1
- package/dist/server-DJ3_UGc4.js.map +0 -1
- package/dist/token-DrFDLVxa.js +0 -365
- package/dist/token-DrFDLVxa.js.map +0 -1
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { P as requestContext, b as HTTPError, g as getCopilotUsage, h as getDeviceCode, j as state, m as getGitHubUser, t as pollAccessToken, v as getProxyEnvDispatcher } from "./poll-access-token-GzVkiTH8.js";
|
|
2
|
+
import { t as PATHS } from "./paths-Bpsb62LK.js";
|
|
3
|
+
import { k as setProviderConfig, m as getRawProviderConfig, w as isResponsesApiWebSocketEnabled } from "./config-BaU_aWgi.js";
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { events } from "fetch-event-stream";
|
|
10
|
+
import { WebSocket } from "undici";
|
|
11
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
|
+
//#region src/services/responses-websocket.ts
|
|
13
|
+
const DEFAULT_WEBSOCKET_IDLE_TIMEOUT_MS = 6e4;
|
|
14
|
+
const websocketPool = /* @__PURE__ */ new Map();
|
|
15
|
+
const websocketActiveRequests = /* @__PURE__ */ new Map();
|
|
16
|
+
const createWebSocketUrl = (url) => {
|
|
17
|
+
const websocketUrl = new URL(url);
|
|
18
|
+
if (websocketUrl.protocol === "https:") websocketUrl.protocol = "wss:";
|
|
19
|
+
else if (websocketUrl.protocol === "http:") websocketUrl.protocol = "ws:";
|
|
20
|
+
return websocketUrl.toString();
|
|
21
|
+
};
|
|
22
|
+
const createPooledWebSocketStream = (request, options) => runPooledWebSocketRequest(request, options);
|
|
23
|
+
const runPooledWebSocketRequest = async function* (request, options) {
|
|
24
|
+
const { entry, pooled } = getPooledWebSocketRequestTarget(request, options);
|
|
25
|
+
const release = acquirePooledWebSocketEntry(request.poolKey, entry, pooled, options);
|
|
26
|
+
try {
|
|
27
|
+
const websocket = await getReadyPooledWebSocket(request.poolKey, entry, pooled, options);
|
|
28
|
+
websocket.send(JSON.stringify(request.payload));
|
|
29
|
+
for await (const data of createWebSocketMessageStream(websocket, options)) {
|
|
30
|
+
const chunk = options.createChunk(data);
|
|
31
|
+
yield chunk;
|
|
32
|
+
if (options.isTerminalChunk(chunk)) return;
|
|
33
|
+
}
|
|
34
|
+
removePooledWebSocketEntry(request.poolKey, entry);
|
|
35
|
+
throw new Error(options.terminalChunkMissingMessage);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
removePooledWebSocketEntry(request.poolKey, entry);
|
|
38
|
+
throw toError(error);
|
|
39
|
+
} finally {
|
|
40
|
+
release();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const getPooledWebSocketRequestTarget = (request, options) => {
|
|
44
|
+
if (getPooledWebSocketActiveRequestCount(request.poolKey) > 0) return {
|
|
45
|
+
entry: createPooledWebSocketEntry(request, options),
|
|
46
|
+
pooled: false
|
|
47
|
+
};
|
|
48
|
+
const existing = websocketPool.get(request.poolKey);
|
|
49
|
+
if (existing && !existing.closed) {
|
|
50
|
+
clearPooledWebSocketIdleTimer(existing);
|
|
51
|
+
return {
|
|
52
|
+
entry: existing,
|
|
53
|
+
pooled: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const entry = createPooledWebSocketEntry(request, options);
|
|
57
|
+
websocketPool.set(request.poolKey, entry);
|
|
58
|
+
return {
|
|
59
|
+
entry,
|
|
60
|
+
pooled: true
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const createPooledWebSocketEntry = (request, options) => {
|
|
64
|
+
const entry = {
|
|
65
|
+
closed: false,
|
|
66
|
+
idleTimer: null,
|
|
67
|
+
requestCount: 0,
|
|
68
|
+
websocketPromise: openWebSocket({
|
|
69
|
+
headers: request.headers,
|
|
70
|
+
openErrorMessage: options.openErrorMessage,
|
|
71
|
+
url: request.url
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
entry.websocketPromise.then((websocket) => {
|
|
75
|
+
websocket.addEventListener("close", () => {
|
|
76
|
+
removePooledWebSocketEntry(request.poolKey, entry);
|
|
77
|
+
});
|
|
78
|
+
websocket.addEventListener("error", () => {
|
|
79
|
+
removePooledWebSocketEntry(request.poolKey, entry);
|
|
80
|
+
});
|
|
81
|
+
}).catch(() => {
|
|
82
|
+
removePooledWebSocketEntry(request.poolKey, entry);
|
|
83
|
+
});
|
|
84
|
+
return entry;
|
|
85
|
+
};
|
|
86
|
+
const acquirePooledWebSocketEntry = (poolKey, entry, pooled, options) => {
|
|
87
|
+
clearPooledWebSocketIdleTimer(entry);
|
|
88
|
+
incrementPooledWebSocketActiveRequestCount(poolKey);
|
|
89
|
+
entry.requestCount += 1;
|
|
90
|
+
let released = false;
|
|
91
|
+
return () => {
|
|
92
|
+
if (released) return;
|
|
93
|
+
released = true;
|
|
94
|
+
entry.requestCount -= 1;
|
|
95
|
+
decrementPooledWebSocketActiveRequestCount(poolKey);
|
|
96
|
+
if (entry.closed || entry.requestCount > 0) return;
|
|
97
|
+
if (pooled && websocketPool.get(poolKey) === entry) {
|
|
98
|
+
schedulePooledWebSocketIdleClose(poolKey, entry, options);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
removePooledWebSocketEntry(poolKey, entry);
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const getReadyPooledWebSocket = async (poolKey, entry, pooled, options) => {
|
|
105
|
+
const unavailableErrorMessage = options?.unavailableErrorMessage ?? "Websocket connection became unavailable before the request started";
|
|
106
|
+
if (entry.closed) throw new Error(unavailableErrorMessage);
|
|
107
|
+
const websocket = await entry.websocketPromise;
|
|
108
|
+
if (entry.closed || pooled && websocketPool.get(poolKey) !== entry) throw new Error(unavailableErrorMessage);
|
|
109
|
+
if (websocket.readyState !== WebSocket.OPEN) {
|
|
110
|
+
removePooledWebSocketEntry(poolKey, entry);
|
|
111
|
+
throw new Error(unavailableErrorMessage);
|
|
112
|
+
}
|
|
113
|
+
return websocket;
|
|
114
|
+
};
|
|
115
|
+
const schedulePooledWebSocketIdleClose = (poolKey, entry, options) => {
|
|
116
|
+
clearPooledWebSocketIdleTimer(entry);
|
|
117
|
+
entry.idleTimer = setTimeout(() => {
|
|
118
|
+
removePooledWebSocketEntry(poolKey, entry);
|
|
119
|
+
}, options.idleTimeoutMs ?? DEFAULT_WEBSOCKET_IDLE_TIMEOUT_MS);
|
|
120
|
+
unrefTimer(entry.idleTimer);
|
|
121
|
+
};
|
|
122
|
+
const clearPooledWebSocketIdleTimer = (entry) => {
|
|
123
|
+
if (entry.idleTimer) {
|
|
124
|
+
clearTimeout(entry.idleTimer);
|
|
125
|
+
entry.idleTimer = null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const getPooledWebSocketActiveRequestCount = (poolKey) => websocketActiveRequests.get(poolKey) ?? 0;
|
|
129
|
+
const incrementPooledWebSocketActiveRequestCount = (poolKey) => {
|
|
130
|
+
websocketActiveRequests.set(poolKey, getPooledWebSocketActiveRequestCount(poolKey) + 1);
|
|
131
|
+
};
|
|
132
|
+
const decrementPooledWebSocketActiveRequestCount = (poolKey) => {
|
|
133
|
+
const nextCount = getPooledWebSocketActiveRequestCount(poolKey) - 1;
|
|
134
|
+
if (nextCount <= 0) {
|
|
135
|
+
websocketActiveRequests.delete(poolKey);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
websocketActiveRequests.set(poolKey, nextCount);
|
|
139
|
+
};
|
|
140
|
+
const removePooledWebSocketEntry = (poolKey, entry) => {
|
|
141
|
+
if (websocketPool.get(poolKey) === entry) websocketPool.delete(poolKey);
|
|
142
|
+
if (entry.closed) return;
|
|
143
|
+
entry.closed = true;
|
|
144
|
+
clearPooledWebSocketIdleTimer(entry);
|
|
145
|
+
entry.websocketPromise.then(closeWebSocket).catch(() => {});
|
|
146
|
+
};
|
|
147
|
+
const unrefTimer = (timer) => {
|
|
148
|
+
if (typeof timer === "object" && "unref" in timer && typeof timer.unref === "function") timer.unref();
|
|
149
|
+
};
|
|
150
|
+
const createWebSocketError = (message, event) => {
|
|
151
|
+
const reason = event?.error ?? event?.message;
|
|
152
|
+
if (reason === void 0 || reason === "") return new Error(message);
|
|
153
|
+
const cause = toError(reason);
|
|
154
|
+
return new Error(`${message}: ${cause.message}`, { cause });
|
|
155
|
+
};
|
|
156
|
+
const openWebSocket = async ({ headers, openErrorMessage, url }) => await new Promise((resolve, reject) => {
|
|
157
|
+
const dispatcher = getProxyEnvDispatcher();
|
|
158
|
+
const websocket = new WebSocket(url, dispatcher ? {
|
|
159
|
+
dispatcher,
|
|
160
|
+
headers
|
|
161
|
+
} : { headers });
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
websocket.removeEventListener("open", onOpen);
|
|
164
|
+
websocket.removeEventListener("error", onError);
|
|
165
|
+
};
|
|
166
|
+
const onOpen = () => {
|
|
167
|
+
cleanup();
|
|
168
|
+
resolve(websocket);
|
|
169
|
+
};
|
|
170
|
+
const onError = (event) => {
|
|
171
|
+
cleanup();
|
|
172
|
+
reject(createWebSocketError(openErrorMessage, event));
|
|
173
|
+
};
|
|
174
|
+
websocket.addEventListener("open", onOpen);
|
|
175
|
+
websocket.addEventListener("error", onError);
|
|
176
|
+
});
|
|
177
|
+
const createWebSocketMessageStream = async function* (websocket, options) {
|
|
178
|
+
const queue = [];
|
|
179
|
+
let closed = false;
|
|
180
|
+
let error = null;
|
|
181
|
+
let notify = null;
|
|
182
|
+
const wake = () => {
|
|
183
|
+
notify?.();
|
|
184
|
+
notify = null;
|
|
185
|
+
};
|
|
186
|
+
const onMessage = (event) => {
|
|
187
|
+
queue.push(normalizeWebSocketMessageData(event.data));
|
|
188
|
+
wake();
|
|
189
|
+
};
|
|
190
|
+
const onClose = () => {
|
|
191
|
+
closed = true;
|
|
192
|
+
wake();
|
|
193
|
+
};
|
|
194
|
+
const onError = (event) => {
|
|
195
|
+
error = createWebSocketError(options.streamErrorMessage, event);
|
|
196
|
+
wake();
|
|
197
|
+
};
|
|
198
|
+
websocket.addEventListener("message", onMessage);
|
|
199
|
+
websocket.addEventListener("close", onClose);
|
|
200
|
+
websocket.addEventListener("error", onError);
|
|
201
|
+
try {
|
|
202
|
+
while (true) {
|
|
203
|
+
const item = queue.shift();
|
|
204
|
+
if (item) {
|
|
205
|
+
yield await item;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (error) throw toError(error);
|
|
209
|
+
if (closed) break;
|
|
210
|
+
await new Promise((resolve) => {
|
|
211
|
+
notify = resolve;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
websocket.removeEventListener("message", onMessage);
|
|
216
|
+
websocket.removeEventListener("close", onClose);
|
|
217
|
+
websocket.removeEventListener("error", onError);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const normalizeWebSocketMessageData = async (data) => {
|
|
221
|
+
if (typeof data === "string") return data;
|
|
222
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
223
|
+
if (ArrayBuffer.isView(data)) {
|
|
224
|
+
const view = data;
|
|
225
|
+
return new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
226
|
+
}
|
|
227
|
+
if (isTextReadable(data)) return await data.text();
|
|
228
|
+
return String(data);
|
|
229
|
+
};
|
|
230
|
+
const isTextReadable = (value) => {
|
|
231
|
+
if (!value || typeof value !== "object" || !("text" in value)) return false;
|
|
232
|
+
return typeof value.text === "function";
|
|
233
|
+
};
|
|
234
|
+
const toError = (value) => {
|
|
235
|
+
if (value instanceof Error) return value;
|
|
236
|
+
return new Error(String(value));
|
|
237
|
+
};
|
|
238
|
+
const closeWebSocket = (websocket) => {
|
|
239
|
+
if (websocket.readyState === WebSocket.CONNECTING || websocket.readyState === WebSocket.OPEN) websocket.close();
|
|
240
|
+
};
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/services/codex/create-responses.ts
|
|
243
|
+
const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api";
|
|
244
|
+
const STRIPPED_CODEX_REQUEST_HEADERS = new Set([
|
|
245
|
+
"authorization",
|
|
246
|
+
"connection",
|
|
247
|
+
"content-length",
|
|
248
|
+
"host",
|
|
249
|
+
"keep-alive",
|
|
250
|
+
"proxy-authenticate",
|
|
251
|
+
"proxy-authorization",
|
|
252
|
+
"te",
|
|
253
|
+
"trailer",
|
|
254
|
+
"transfer-encoding",
|
|
255
|
+
"upgrade",
|
|
256
|
+
"x-api-key"
|
|
257
|
+
]);
|
|
258
|
+
const STRIPPED_CODEX_WEBSOCKET_HEADERS = new Set(["accept", "content-type"]);
|
|
259
|
+
const requireCodexAuthContext = () => {
|
|
260
|
+
const accessToken = state.codexAccessToken;
|
|
261
|
+
const accountId = state.codexAccountId;
|
|
262
|
+
if (!accessToken) throw new Error("Codex access token is not loaded");
|
|
263
|
+
if (!accountId) throw new Error("Codex account id is not loaded");
|
|
264
|
+
return {
|
|
265
|
+
accessToken,
|
|
266
|
+
accountId
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
function resolveCodexResponsesUrl(baseUrl = CODEX_API_BASE_URL) {
|
|
270
|
+
const normalized = baseUrl.trim().replace(/\/+$/, "");
|
|
271
|
+
if (!normalized) return `${CODEX_API_BASE_URL}/codex/responses`;
|
|
272
|
+
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
273
|
+
if (normalized.endsWith("/codex")) return `${normalized}/responses`;
|
|
274
|
+
return `${normalized}/codex/responses`;
|
|
275
|
+
}
|
|
276
|
+
function buildCodexResponsesHeaders(requestHeaders, options = {}) {
|
|
277
|
+
const { accessToken, accountId } = requireCodexAuthContext();
|
|
278
|
+
const headers = new Headers();
|
|
279
|
+
for (const [headerName, headerValue] of requestHeaders) {
|
|
280
|
+
const headerNameLower = headerName.toLowerCase();
|
|
281
|
+
if (STRIPPED_CODEX_REQUEST_HEADERS.has(headerNameLower)) continue;
|
|
282
|
+
if (headerNameLower.includes("trace")) continue;
|
|
283
|
+
headers.set(headerName, headerValue);
|
|
284
|
+
}
|
|
285
|
+
if (!headers.has("accept")) headers.set("accept", options.stream ? "text/event-stream" : "application/json");
|
|
286
|
+
headers.set("authorization", `Bearer ${accessToken}`);
|
|
287
|
+
headers.set("chatgpt-account-id", accountId);
|
|
288
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
289
|
+
if (!headers.has("OpenAI-Beta")) headers.set("OpenAI-Beta", "responses=experimental");
|
|
290
|
+
if (!headers.has("originator")) headers.set("originator", "copilot-api");
|
|
291
|
+
if (!headers.has("user-agent")) headers.set("user-agent", "copilot-api");
|
|
292
|
+
if (headers.get("user-agent")?.startsWith("opencode")) {
|
|
293
|
+
headers.set("originator", "opencode");
|
|
294
|
+
const sessionId = requestContext.getStore()?.sessionAffinity;
|
|
295
|
+
if (sessionId) headers.set("session-id", sessionId);
|
|
296
|
+
}
|
|
297
|
+
return headers;
|
|
298
|
+
}
|
|
299
|
+
function resolveCodexResponsesTransport(transport) {
|
|
300
|
+
return transport ?? (isResponsesApiWebSocketEnabled() ? "websocket" : "http");
|
|
301
|
+
}
|
|
302
|
+
function buildCodexResponsesWebSocketHeaders(requestHeaders) {
|
|
303
|
+
const headers = buildCodexResponsesHeaders(requestHeaders);
|
|
304
|
+
for (const headerName of STRIPPED_CODEX_WEBSOCKET_HEADERS) headers.delete(headerName);
|
|
305
|
+
return Object.fromEntries(headers);
|
|
306
|
+
}
|
|
307
|
+
function buildCodexResponsesWebSocketPayload(payload) {
|
|
308
|
+
const websocketPayload = {
|
|
309
|
+
...normalizeCodexResponsesPayload(payload),
|
|
310
|
+
type: "response.create"
|
|
311
|
+
};
|
|
312
|
+
delete websocketPayload.stream;
|
|
313
|
+
return websocketPayload;
|
|
314
|
+
}
|
|
315
|
+
function buildCodexResponsesWebSocketUrl(baseUrl = CODEX_API_BASE_URL) {
|
|
316
|
+
return createWebSocketUrl(resolveCodexResponsesUrl(baseUrl));
|
|
317
|
+
}
|
|
318
|
+
function prepareCodexResponsesWebSocketRequest(payload, requestHeaders, baseUrl = CODEX_API_BASE_URL) {
|
|
319
|
+
const headers = buildCodexResponsesWebSocketHeaders(requestHeaders);
|
|
320
|
+
return {
|
|
321
|
+
headers,
|
|
322
|
+
payload: buildCodexResponsesWebSocketPayload(payload),
|
|
323
|
+
poolKey: buildCodexResponsesWebSocketPoolKey(payload, headers, baseUrl),
|
|
324
|
+
url: buildCodexResponsesWebSocketUrl(baseUrl)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function forwardCodexResponses(payload, requestHeaders, baseUrl = CODEX_API_BASE_URL, options = {}) {
|
|
328
|
+
const transport = resolveCodexResponsesTransport(options.transport);
|
|
329
|
+
if (payload.stream && transport === "websocket") return forwardCodexResponsesOverWebSocket(payload, requestHeaders, baseUrl);
|
|
330
|
+
const normalizedPayload = normalizeCodexResponsesPayload(payload);
|
|
331
|
+
const response = await fetch(resolveCodexResponsesUrl(baseUrl), {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: buildCodexResponsesHeaders(requestHeaders, { stream: normalizedPayload.stream }),
|
|
334
|
+
body: JSON.stringify(normalizedPayload)
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) throw new HTTPError("Failed to create codex responses", response);
|
|
337
|
+
if (normalizedPayload.stream) return events(response);
|
|
338
|
+
return await response.json();
|
|
339
|
+
}
|
|
340
|
+
const normalizeCodexResponsesPayload = (payload) => {
|
|
341
|
+
const normalizedPayload = {
|
|
342
|
+
...payload,
|
|
343
|
+
store: false
|
|
344
|
+
};
|
|
345
|
+
delete normalizedPayload.temperature;
|
|
346
|
+
delete normalizedPayload.top_p;
|
|
347
|
+
delete normalizedPayload.max_output_tokens;
|
|
348
|
+
delete normalizedPayload.metadata;
|
|
349
|
+
if (typeof normalizedPayload.instructions === "string" && normalizedPayload.instructions.trim().length > 0 || !Array.isArray(normalizedPayload.input)) return normalizedPayload;
|
|
350
|
+
const instructions = [];
|
|
351
|
+
let messageCount = 0;
|
|
352
|
+
const remainingInput = normalizedPayload.input.filter((inputItem) => {
|
|
353
|
+
const message = getResponseInputMessage(inputItem);
|
|
354
|
+
if (!message) return true;
|
|
355
|
+
messageCount += 1;
|
|
356
|
+
if (message.role !== "system" || messageCount > 3) return true;
|
|
357
|
+
const systemPrompt = getTextContent(message.content);
|
|
358
|
+
if (systemPrompt === void 0) return true;
|
|
359
|
+
if (systemPrompt.trim().length > 0) instructions.push(systemPrompt);
|
|
360
|
+
return false;
|
|
361
|
+
});
|
|
362
|
+
if (remainingInput.length === normalizedPayload.input.length) return normalizedPayload;
|
|
363
|
+
if (instructions.length > 0) normalizedPayload.instructions = instructions.join("\n\n");
|
|
364
|
+
if (remainingInput.length > 0) normalizedPayload.input = remainingInput;
|
|
365
|
+
else delete normalizedPayload.input;
|
|
366
|
+
return normalizedPayload;
|
|
367
|
+
};
|
|
368
|
+
const getResponseInputMessage = (inputItem) => {
|
|
369
|
+
if (typeof inputItem !== "object" || inputItem === null) return;
|
|
370
|
+
const { role, type } = inputItem;
|
|
371
|
+
if (typeof role !== "string" || type !== void 0 && type !== "message") return;
|
|
372
|
+
return inputItem;
|
|
373
|
+
};
|
|
374
|
+
const getTextContent = (content) => {
|
|
375
|
+
if (typeof content === "string") return content;
|
|
376
|
+
if (content === void 0) return "";
|
|
377
|
+
if (!Array.isArray(content)) return;
|
|
378
|
+
const textBlocks = [];
|
|
379
|
+
for (const contentBlock of content) {
|
|
380
|
+
const text = getTextBlock(contentBlock);
|
|
381
|
+
if (text === void 0) return;
|
|
382
|
+
if (text.length > 0) textBlocks.push(text);
|
|
383
|
+
}
|
|
384
|
+
return textBlocks.join("\n\n");
|
|
385
|
+
};
|
|
386
|
+
const getTextBlock = (contentBlock) => {
|
|
387
|
+
if (typeof contentBlock !== "object" || contentBlock === null) return;
|
|
388
|
+
const { text, type } = contentBlock;
|
|
389
|
+
if (type !== void 0 && type !== "input_text" && type !== "output_text") return;
|
|
390
|
+
return typeof text === "string" ? text : void 0;
|
|
391
|
+
};
|
|
392
|
+
const buildCodexResponsesWebSocketPoolKey = (payload, headers, baseUrl) => {
|
|
393
|
+
const authFingerprint = createHash("sha256").update(`${state.codexAccessToken ?? "missing-token"}:${state.codexAccountId ?? "missing-account"}`).digest("hex").slice(0, 16);
|
|
394
|
+
const headerFingerprint = createHash("sha256").update(JSON.stringify(Object.entries(headers).filter(([headerName]) => !headerName.toLowerCase().includes("trace")).sort(([left], [right]) => left.localeCompare(right)))).digest("hex").slice(0, 16);
|
|
395
|
+
return [
|
|
396
|
+
"codex",
|
|
397
|
+
resolveCodexResponsesUrl(baseUrl),
|
|
398
|
+
payload.model,
|
|
399
|
+
authFingerprint,
|
|
400
|
+
headerFingerprint
|
|
401
|
+
].map(encodePoolKeyPart).join("|");
|
|
402
|
+
};
|
|
403
|
+
const forwardCodexResponsesOverWebSocket = (payload, requestHeaders, baseUrl) => {
|
|
404
|
+
return createCodexResponsesWebSocketStream(prepareCodexResponsesWebSocketRequest(payload, requestHeaders, baseUrl));
|
|
405
|
+
};
|
|
406
|
+
const createCodexResponsesWebSocketStream = (request) => createCodexResponsesSafeStream(createPooledWebSocketStream(request, {
|
|
407
|
+
createChunk: createCodexResponsesWebSocketStreamChunk,
|
|
408
|
+
isTerminalChunk: isTerminalCodexResponsesWebSocketChunk,
|
|
409
|
+
openErrorMessage: "Failed to create codex responses websocket",
|
|
410
|
+
streamErrorMessage: "Codex responses websocket stream error",
|
|
411
|
+
terminalChunkMissingMessage: "Codex responses websocket ended without a terminal response"
|
|
412
|
+
}));
|
|
413
|
+
const createCodexResponsesSafeStream = async function* (source) {
|
|
414
|
+
try {
|
|
415
|
+
yield* source;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
yield createResponsesErrorServerSentEventChunk(getErrorMessage(error));
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const createCodexResponsesWebSocketStreamChunk = (data) => {
|
|
421
|
+
if (data === "[DONE]") return { data };
|
|
422
|
+
try {
|
|
423
|
+
const parsed = JSON.parse(data);
|
|
424
|
+
if (parsed.type === "error" && parsed.error) parsed.message = parsed.error.message;
|
|
425
|
+
return {
|
|
426
|
+
data: JSON.stringify(parsed),
|
|
427
|
+
event: typeof parsed.type === "string" ? parsed.type : void 0,
|
|
428
|
+
id: typeof parsed.id === "string" ? parsed.id : void 0
|
|
429
|
+
};
|
|
430
|
+
} catch {
|
|
431
|
+
return { data };
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const isTerminalCodexResponsesWebSocketChunk = (chunk) => {
|
|
435
|
+
if (!chunk.data || chunk.data === "[DONE]") return false;
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse(chunk.data);
|
|
438
|
+
return parsed.type === "response.completed" || parsed.type === "response.failed" || parsed.type === "response.incomplete" || parsed.type === "error";
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const createResponsesErrorServerSentEventChunk = (message) => {
|
|
444
|
+
const errorEvent = {
|
|
445
|
+
code: null,
|
|
446
|
+
message,
|
|
447
|
+
param: null,
|
|
448
|
+
sequence_number: 0,
|
|
449
|
+
type: "error"
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
data: JSON.stringify(errorEvent),
|
|
453
|
+
event: errorEvent.type
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
const getErrorMessage = (error) => {
|
|
457
|
+
if (error instanceof Error && error.message) return error.message;
|
|
458
|
+
return String(error);
|
|
459
|
+
};
|
|
460
|
+
const encodePoolKeyPart = (value) => encodeURIComponent(value);
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/lib/oauth/codex.ts
|
|
463
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
464
|
+
const CALLBACK_PORT = 1455;
|
|
465
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
466
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
467
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
468
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
469
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
470
|
+
const SCOPE = "openid profile email offline_access";
|
|
471
|
+
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
472
|
+
const REFRESH_BUFFER_MS = 6e4;
|
|
473
|
+
function base64UrlEncode(bytes) {
|
|
474
|
+
return Buffer.from(bytes).toString("base64url");
|
|
475
|
+
}
|
|
476
|
+
async function generatePkce() {
|
|
477
|
+
const verifierBytes = new Uint8Array(32);
|
|
478
|
+
crypto.getRandomValues(verifierBytes);
|
|
479
|
+
const verifier = base64UrlEncode(verifierBytes);
|
|
480
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
481
|
+
return {
|
|
482
|
+
verifier,
|
|
483
|
+
challenge: base64UrlEncode(new Uint8Array(hashBuffer))
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function createState() {
|
|
487
|
+
return randomBytes(16).toString("hex");
|
|
488
|
+
}
|
|
489
|
+
function parseAuthorizationInput(input) {
|
|
490
|
+
const value = input.trim();
|
|
491
|
+
if (!value) return {};
|
|
492
|
+
try {
|
|
493
|
+
const url = new URL(value);
|
|
494
|
+
return {
|
|
495
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
496
|
+
state: url.searchParams.get("state") ?? void 0
|
|
497
|
+
};
|
|
498
|
+
} catch {}
|
|
499
|
+
if (value.includes("#")) {
|
|
500
|
+
const [code, state] = value.split("#", 2);
|
|
501
|
+
return {
|
|
502
|
+
code,
|
|
503
|
+
state
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (value.includes("code=")) {
|
|
507
|
+
const params = new URLSearchParams(value);
|
|
508
|
+
return {
|
|
509
|
+
code: params.get("code") ?? void 0,
|
|
510
|
+
state: params.get("state") ?? void 0
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return { code: value };
|
|
514
|
+
}
|
|
515
|
+
function decodeJwt(accessToken) {
|
|
516
|
+
try {
|
|
517
|
+
const payload = accessToken.split(".")[1];
|
|
518
|
+
if (!payload) return null;
|
|
519
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
520
|
+
} catch {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function getAccountId(accessToken) {
|
|
525
|
+
const payload = decodeJwt(accessToken);
|
|
526
|
+
if (!payload) return null;
|
|
527
|
+
const authPayload = payload[JWT_CLAIM_PATH];
|
|
528
|
+
if (!authPayload || typeof authPayload !== "object") return null;
|
|
529
|
+
const accountId = authPayload.chatgpt_account_id;
|
|
530
|
+
return typeof accountId === "string" && accountId ? accountId : null;
|
|
531
|
+
}
|
|
532
|
+
function renderOAuthPage(options) {
|
|
533
|
+
return `<!doctype html>
|
|
534
|
+
<html lang="en">
|
|
535
|
+
<head>
|
|
536
|
+
<meta charset="utf-8" />
|
|
537
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
538
|
+
<title>${escapeHtml(options.title)}</title>
|
|
539
|
+
<style>
|
|
540
|
+
body {
|
|
541
|
+
margin: 0;
|
|
542
|
+
min-height: 100vh;
|
|
543
|
+
display: flex;
|
|
544
|
+
align-items: center;
|
|
545
|
+
justify-content: center;
|
|
546
|
+
padding: 24px;
|
|
547
|
+
background: #09090b;
|
|
548
|
+
color: #fafafa;
|
|
549
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
550
|
+
text-align: center;
|
|
551
|
+
}
|
|
552
|
+
main {
|
|
553
|
+
max-width: 560px;
|
|
554
|
+
}
|
|
555
|
+
h1 {
|
|
556
|
+
margin: 0 0 12px;
|
|
557
|
+
font-size: 28px;
|
|
558
|
+
line-height: 1.15;
|
|
559
|
+
}
|
|
560
|
+
p {
|
|
561
|
+
margin: 0;
|
|
562
|
+
color: #a1a1aa;
|
|
563
|
+
line-height: 1.6;
|
|
564
|
+
}
|
|
565
|
+
</style>
|
|
566
|
+
</head>
|
|
567
|
+
<body>
|
|
568
|
+
<main>
|
|
569
|
+
<h1>${escapeHtml(options.heading)}</h1>
|
|
570
|
+
<p>${escapeHtml(options.message)}</p>
|
|
571
|
+
</main>
|
|
572
|
+
</body>
|
|
573
|
+
</html>`;
|
|
574
|
+
}
|
|
575
|
+
function escapeHtml(value) {
|
|
576
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
577
|
+
}
|
|
578
|
+
function renderOAuthSuccessPage(message) {
|
|
579
|
+
return renderOAuthPage({
|
|
580
|
+
title: "Authentication successful",
|
|
581
|
+
heading: "Authentication successful",
|
|
582
|
+
message
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function renderOAuthErrorPage(message) {
|
|
586
|
+
return renderOAuthPage({
|
|
587
|
+
title: "Authentication failed",
|
|
588
|
+
heading: "Authentication failed",
|
|
589
|
+
message
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
async function exchangeAuthorizationCode(code, verifier) {
|
|
593
|
+
const response = await fetch(TOKEN_URL, {
|
|
594
|
+
method: "POST",
|
|
595
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
596
|
+
body: new URLSearchParams({
|
|
597
|
+
grant_type: "authorization_code",
|
|
598
|
+
client_id: CLIENT_ID,
|
|
599
|
+
code,
|
|
600
|
+
code_verifier: verifier,
|
|
601
|
+
redirect_uri: REDIRECT_URI
|
|
602
|
+
})
|
|
603
|
+
});
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
const details = await response.text().catch(() => "");
|
|
606
|
+
throw new Error(`Codex token exchange failed (${response.status}): ${details || response.statusText}`);
|
|
607
|
+
}
|
|
608
|
+
const payload = await response.json();
|
|
609
|
+
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError(`Codex token exchange response missing fields: ${JSON.stringify(payload)}`);
|
|
610
|
+
return {
|
|
611
|
+
accessToken: payload.access_token,
|
|
612
|
+
refreshToken: payload.refresh_token,
|
|
613
|
+
expiresAt: Date.now() + payload.expires_in * 1e3
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function refreshAccessToken(refreshToken) {
|
|
617
|
+
const response = await fetch(TOKEN_URL, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
620
|
+
body: new URLSearchParams({
|
|
621
|
+
grant_type: "refresh_token",
|
|
622
|
+
refresh_token: refreshToken,
|
|
623
|
+
client_id: CLIENT_ID
|
|
624
|
+
})
|
|
625
|
+
});
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
const details = await response.text().catch(() => "");
|
|
628
|
+
throw new Error(`Codex token refresh failed (${response.status}): ${details || response.statusText}`);
|
|
629
|
+
}
|
|
630
|
+
const payload = await response.json();
|
|
631
|
+
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number") throw new TypeError(`Codex token refresh response missing fields: ${JSON.stringify(payload)}`);
|
|
632
|
+
return {
|
|
633
|
+
accessToken: payload.access_token,
|
|
634
|
+
refreshToken: payload.refresh_token,
|
|
635
|
+
expiresAt: Date.now() + payload.expires_in * 1e3
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
async function createAuthorizationFlow() {
|
|
639
|
+
const { verifier, challenge } = await generatePkce();
|
|
640
|
+
const state = createState();
|
|
641
|
+
const url = new URL(AUTHORIZE_URL);
|
|
642
|
+
url.searchParams.set("response_type", "code");
|
|
643
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
644
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
645
|
+
url.searchParams.set("scope", SCOPE);
|
|
646
|
+
url.searchParams.set("code_challenge", challenge);
|
|
647
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
648
|
+
url.searchParams.set("state", state);
|
|
649
|
+
url.searchParams.set("id_token_add_organizations", "true");
|
|
650
|
+
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
651
|
+
url.searchParams.set("originator", "copilot-api");
|
|
652
|
+
return {
|
|
653
|
+
verifier,
|
|
654
|
+
state,
|
|
655
|
+
url: url.toString()
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function waitForAuthorizationCode(state) {
|
|
659
|
+
let resolveCode;
|
|
660
|
+
const waitForCode = new Promise((resolve) => {
|
|
661
|
+
resolveCode = resolve;
|
|
662
|
+
});
|
|
663
|
+
const server = createServer((request, response) => {
|
|
664
|
+
try {
|
|
665
|
+
const url = new URL(request.url || "", "http://localhost");
|
|
666
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
667
|
+
response.statusCode = 404;
|
|
668
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
669
|
+
response.end(renderOAuthErrorPage("Callback route not found."));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (url.searchParams.get("state") !== state) {
|
|
673
|
+
response.statusCode = 400;
|
|
674
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
675
|
+
response.end(renderOAuthErrorPage("State mismatch."));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const code = url.searchParams.get("code");
|
|
679
|
+
if (!code) {
|
|
680
|
+
response.statusCode = 400;
|
|
681
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
682
|
+
response.end(renderOAuthErrorPage("Missing authorization code."));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
response.statusCode = 200;
|
|
686
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
687
|
+
response.end(renderOAuthSuccessPage("OpenAI Codex authentication completed. You can close this window."));
|
|
688
|
+
resolveCode?.(code);
|
|
689
|
+
} catch {
|
|
690
|
+
response.statusCode = 500;
|
|
691
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
692
|
+
response.end(renderOAuthErrorPage("Internal error while processing OAuth callback."));
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
try {
|
|
696
|
+
await new Promise((resolve, reject) => {
|
|
697
|
+
server.once("error", reject);
|
|
698
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
699
|
+
server.off("error", reject);
|
|
700
|
+
resolve();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
return await waitForCode;
|
|
708
|
+
} finally {
|
|
709
|
+
await new Promise((resolve, reject) => {
|
|
710
|
+
server.close((error) => {
|
|
711
|
+
if (error) {
|
|
712
|
+
reject(error);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
resolve();
|
|
716
|
+
});
|
|
717
|
+
}).catch(() => void 0);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function loginCodex(options) {
|
|
721
|
+
const { verifier, state, url } = await createAuthorizationFlow();
|
|
722
|
+
options.onAuth({
|
|
723
|
+
url,
|
|
724
|
+
instructions: "Please complete the login in the browser. If the browser does not automatically redirect, please paste the callback URL or code back to the terminal."
|
|
725
|
+
});
|
|
726
|
+
options.onProgress?.("Waiting for Codex OAuth callback");
|
|
727
|
+
let code = await waitForAuthorizationCode(state);
|
|
728
|
+
if (!code) {
|
|
729
|
+
const parsed = parseAuthorizationInput(await options.onPrompt("Paste the authorization code or full redirect URL:"));
|
|
730
|
+
if (parsed.state && parsed.state !== state) throw new Error("Codex OAuth state mismatch");
|
|
731
|
+
code = parsed.code ?? null;
|
|
732
|
+
}
|
|
733
|
+
if (!code) throw new Error("Missing Codex authorization code");
|
|
734
|
+
const tokenResult = await exchangeAuthorizationCode(code, verifier);
|
|
735
|
+
const accountId = getAccountId(tokenResult.accessToken);
|
|
736
|
+
if (!accountId) throw new Error("Failed to extract Codex account id from access token");
|
|
737
|
+
return {
|
|
738
|
+
accessToken: tokenResult.accessToken,
|
|
739
|
+
refreshToken: tokenResult.refreshToken,
|
|
740
|
+
expiresAt: tokenResult.expiresAt,
|
|
741
|
+
accountId
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function refreshCodexCredentials(credentials) {
|
|
745
|
+
const tokenResult = await refreshAccessToken(credentials.refreshToken);
|
|
746
|
+
const accountId = getAccountId(tokenResult.accessToken);
|
|
747
|
+
if (!accountId) throw new Error("Failed to extract Codex account id from access token");
|
|
748
|
+
return {
|
|
749
|
+
accessToken: tokenResult.accessToken,
|
|
750
|
+
refreshToken: tokenResult.refreshToken,
|
|
751
|
+
expiresAt: tokenResult.expiresAt,
|
|
752
|
+
accountId
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function isCodexCredentialsExpired(credentials, now = Date.now()) {
|
|
756
|
+
return credentials.expiresAt <= now + REFRESH_BUFFER_MS;
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/lib/credential-store.ts
|
|
760
|
+
function isNodeError(error) {
|
|
761
|
+
return error instanceof Error && "code" in error;
|
|
762
|
+
}
|
|
763
|
+
async function readOptionalFile(filePath) {
|
|
764
|
+
try {
|
|
765
|
+
return await fs.readFile(filePath, "utf8");
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (isNodeError(error) && error.code === "ENOENT") return null;
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function writeProtectedFile(filePath, content) {
|
|
772
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
773
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
774
|
+
try {
|
|
775
|
+
await fs.chmod(filePath, 384);
|
|
776
|
+
} catch {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function normalizeCodexCredentials(credentials) {
|
|
781
|
+
if (!credentials || typeof credentials !== "object") return null;
|
|
782
|
+
const candidate = credentials;
|
|
783
|
+
if (typeof candidate.accessToken !== "string" || typeof candidate.refreshToken !== "string" || typeof candidate.expiresAt !== "number" || typeof candidate.accountId !== "string") return null;
|
|
784
|
+
return {
|
|
785
|
+
accessToken: candidate.accessToken,
|
|
786
|
+
refreshToken: candidate.refreshToken,
|
|
787
|
+
expiresAt: candidate.expiresAt,
|
|
788
|
+
accountId: candidate.accountId
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async function readGitHubToken() {
|
|
792
|
+
return (await readOptionalFile(PATHS.GITHUB_TOKEN_PATH))?.trim() || null;
|
|
793
|
+
}
|
|
794
|
+
async function writeGitHubToken(token) {
|
|
795
|
+
await writeProtectedFile(PATHS.GITHUB_TOKEN_PATH, token.trim());
|
|
796
|
+
}
|
|
797
|
+
async function readCodexCredentials() {
|
|
798
|
+
const raw = await readOptionalFile(PATHS.CODEX_CREDENTIAL_PATH);
|
|
799
|
+
if (!raw?.trim()) return null;
|
|
800
|
+
let parsed;
|
|
801
|
+
try {
|
|
802
|
+
parsed = JSON.parse(raw);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
throw new Error(`Codex credentials file is not valid JSON: ${PATHS.CODEX_CREDENTIAL_PATH}`, { cause: error });
|
|
805
|
+
}
|
|
806
|
+
const credentials = normalizeCodexCredentials(parsed);
|
|
807
|
+
if (!credentials) throw new Error(`Codex credentials file is missing required fields: ${PATHS.CODEX_CREDENTIAL_PATH}`);
|
|
808
|
+
return credentials;
|
|
809
|
+
}
|
|
810
|
+
async function writeCodexCredentials(credentials) {
|
|
811
|
+
await writeProtectedFile(PATHS.CODEX_CREDENTIAL_PATH, `${JSON.stringify(credentials, null, 2)}\n`);
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/lib/token.ts
|
|
815
|
+
let codexRefreshLoopController = null;
|
|
816
|
+
const stopCodexRefreshLoop = () => {
|
|
817
|
+
if (!codexRefreshLoopController) return;
|
|
818
|
+
codexRefreshLoopController.abort();
|
|
819
|
+
codexRefreshLoopController = null;
|
|
820
|
+
};
|
|
821
|
+
function applyCodexCredentials(credentials) {
|
|
822
|
+
state.codexAccessToken = credentials.accessToken;
|
|
823
|
+
state.codexRefreshToken = credentials.refreshToken;
|
|
824
|
+
state.codexExpiresAt = credentials.expiresAt;
|
|
825
|
+
state.codexAccountId = credentials.accountId;
|
|
826
|
+
consola.debug("Codex credentials loaded successfully");
|
|
827
|
+
if (state.showToken) consola.info("Codex access token:", credentials.accessToken);
|
|
828
|
+
}
|
|
829
|
+
function getLoadedCodexCredentials() {
|
|
830
|
+
if (!state.codexAccessToken || !state.codexRefreshToken || !state.codexExpiresAt || !state.codexAccountId) return null;
|
|
831
|
+
return {
|
|
832
|
+
accessToken: state.codexAccessToken,
|
|
833
|
+
refreshToken: state.codexRefreshToken,
|
|
834
|
+
expiresAt: state.codexExpiresAt,
|
|
835
|
+
accountId: state.codexAccountId
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function syncCodexProviderConfig(options) {
|
|
839
|
+
const existingProviderConfig = getRawProviderConfig("codex") ?? {};
|
|
840
|
+
setProviderConfig("codex", {
|
|
841
|
+
...existingProviderConfig,
|
|
842
|
+
type: "openai-responses",
|
|
843
|
+
enabled: options?.enabled ?? existingProviderConfig.enabled,
|
|
844
|
+
baseUrl: CODEX_API_BASE_URL,
|
|
845
|
+
authType: "oauth2"
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function persistCodexCredentials(credentials, options) {
|
|
849
|
+
await writeCodexCredentials(credentials);
|
|
850
|
+
syncCodexProviderConfig({ enabled: options?.enableProvider ? true : void 0 });
|
|
851
|
+
applyCodexCredentials(credentials);
|
|
852
|
+
}
|
|
853
|
+
const setupCodexToken = async () => {
|
|
854
|
+
const loadedCredentials = getLoadedCodexCredentials();
|
|
855
|
+
if (loadedCredentials && !isCodexCredentialsExpired(loadedCredentials)) {
|
|
856
|
+
if (codexRefreshLoopController) return;
|
|
857
|
+
applyCodexCredentials(loadedCredentials);
|
|
858
|
+
}
|
|
859
|
+
const credentials = loadedCredentials ?? await readCodexCredentials();
|
|
860
|
+
if (!credentials) throw new Error(`Codex credentials not found. Run \`copilot-api auth login --provider codex\` first.`);
|
|
861
|
+
syncCodexProviderConfig();
|
|
862
|
+
let nextCredentials = credentials;
|
|
863
|
+
if (isCodexCredentialsExpired(credentials)) {
|
|
864
|
+
consola.debug("Refreshing expired Codex credentials");
|
|
865
|
+
nextCredentials = await refreshCodexCredentials(credentials);
|
|
866
|
+
await persistCodexCredentials(nextCredentials);
|
|
867
|
+
}
|
|
868
|
+
applyCodexCredentials(nextCredentials);
|
|
869
|
+
stopCodexRefreshLoop();
|
|
870
|
+
const controller = new AbortController();
|
|
871
|
+
codexRefreshLoopController = controller;
|
|
872
|
+
runCodexRefreshLoop(controller.signal).catch(() => {
|
|
873
|
+
consola.warn("Codex token refresh loop stopped");
|
|
874
|
+
}).finally(() => {
|
|
875
|
+
if (codexRefreshLoopController === controller) codexRefreshLoopController = null;
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
const REFRESH_POLL_INTERVAL_MS = 15e3;
|
|
879
|
+
const EARLY_REFRESH_BUFFER_MS = 6e4;
|
|
880
|
+
const RETRY_REFRESH_DELAY_MS = 15e3;
|
|
881
|
+
const getRefreshPollDelayMs = (refreshAtMs, nowMs = Date.now()) => Math.min(Math.max(refreshAtMs - nowMs, 0), REFRESH_POLL_INTERVAL_MS);
|
|
882
|
+
const runCodexRefreshLoop = async (signal) => {
|
|
883
|
+
let refreshAtMs = Math.max((state.codexExpiresAt ?? Date.now()) - EARLY_REFRESH_BUFFER_MS, Date.now());
|
|
884
|
+
while (!signal.aborted) {
|
|
885
|
+
const expiresAt = state.codexExpiresAt;
|
|
886
|
+
const refreshToken = state.codexRefreshToken;
|
|
887
|
+
if (!expiresAt || !refreshToken) return;
|
|
888
|
+
const nextDelayMs = getRefreshPollDelayMs(refreshAtMs);
|
|
889
|
+
if (nextDelayMs > 0) {
|
|
890
|
+
await setTimeout$1(nextDelayMs, void 0, { signal });
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
consola.debug("Refreshing Codex credentials");
|
|
894
|
+
try {
|
|
895
|
+
const credentials = await refreshCodexCredentials({
|
|
896
|
+
accessToken: state.codexAccessToken ?? "",
|
|
897
|
+
refreshToken,
|
|
898
|
+
expiresAt,
|
|
899
|
+
accountId: state.codexAccountId ?? ""
|
|
900
|
+
});
|
|
901
|
+
await persistCodexCredentials(credentials);
|
|
902
|
+
refreshAtMs = Math.max(credentials.expiresAt - EARLY_REFRESH_BUFFER_MS, Date.now());
|
|
903
|
+
consola.debug("Codex credentials refreshed");
|
|
904
|
+
} catch (error) {
|
|
905
|
+
consola.error("Failed to refresh Codex credentials:", error);
|
|
906
|
+
refreshAtMs = Date.now() + RETRY_REFRESH_DELAY_MS;
|
|
907
|
+
consola.warn(`Retrying Codex token refresh in ${RETRY_REFRESH_DELAY_MS / 1e3}s`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
async function setupGitHubToken(options) {
|
|
912
|
+
try {
|
|
913
|
+
const githubToken = await readGitHubToken();
|
|
914
|
+
if (githubToken && !options?.force) {
|
|
915
|
+
state.githubToken = githubToken;
|
|
916
|
+
if (state.showToken) consola.info("GitHub token:", githubToken);
|
|
917
|
+
await logUser();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
consola.info("Not logged in, getting new access token");
|
|
921
|
+
const response = await getDeviceCode();
|
|
922
|
+
consola.debug("Device code response:", response);
|
|
923
|
+
consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
|
|
924
|
+
const token = await pollAccessToken(response);
|
|
925
|
+
await writeGitHubToken(token);
|
|
926
|
+
state.githubToken = token;
|
|
927
|
+
if (state.showToken) consola.info("GitHub token:", token);
|
|
928
|
+
await logUser();
|
|
929
|
+
} catch (error) {
|
|
930
|
+
if (error instanceof HTTPError) {
|
|
931
|
+
consola.error("Failed to get GitHub token:", await error.response.json());
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
consola.error("Failed to get GitHub token:", error);
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async function logUser() {
|
|
939
|
+
const user = await getGitHubUser();
|
|
940
|
+
state.userName = user.login;
|
|
941
|
+
consola.info(`Logged in as ${user.login}`);
|
|
942
|
+
state.copilotApiUrl = (await getCopilotUsage()).endpoints.api;
|
|
943
|
+
}
|
|
944
|
+
//#endregion
|
|
945
|
+
export { forwardCodexResponses as a, loginCodex as i, setupCodexToken as n, setupGitHubToken as r, persistCodexCredentials as t };
|
|
946
|
+
|
|
947
|
+
//# sourceMappingURL=token-671YFxgv.js.map
|