@dexto/agent-management 1.8.1 → 1.8.2
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/dist/auth/chatgpt-oauth.cjs +332 -0
- package/dist/auth/chatgpt-oauth.d.ts +28 -0
- package/dist/auth/chatgpt-oauth.d.ts.map +1 -0
- package/dist/auth/chatgpt-oauth.js +305 -0
- package/dist/auth/index.cjs +83 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +64 -0
- package/dist/auth/model-auth-handlers.cjs +141 -0
- package/dist/auth/model-auth-handlers.d.ts +17 -0
- package/dist/auth/model-auth-handlers.d.ts.map +1 -0
- package/dist/auth/model-auth-handlers.js +126 -0
- package/dist/auth/model-auth-profiles.cjs +253 -0
- package/dist/auth/model-auth-profiles.d.ts +46 -0
- package/dist/auth/model-auth-profiles.d.ts.map +1 -0
- package/dist/auth/model-auth-profiles.js +208 -0
- package/dist/auth/provider-auth-definitions.cjs +72 -0
- package/dist/auth/provider-auth-definitions.d.ts +52 -0
- package/dist/auth/provider-auth-definitions.d.ts.map +1 -0
- package/dist/auth/provider-auth-definitions.js +41 -0
- package/dist/index.cjs +55 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -0
- package/dist/preferences/schemas.cjs +1 -1
- package/dist/preferences/schemas.js +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var chatgpt_oauth_exports = {};
|
|
20
|
+
__export(chatgpt_oauth_exports, {
|
|
21
|
+
CHATGPT_OAUTH_REDIRECT_URI: () => CHATGPT_OAUTH_REDIRECT_URI,
|
|
22
|
+
createChatGPTRuntimeAuth: () => createChatGPTRuntimeAuth,
|
|
23
|
+
refreshChatGPTOAuthCredential: () => refreshChatGPTOAuthCredential,
|
|
24
|
+
startChatGPTBrowserLogin: () => startChatGPTBrowserLogin
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(chatgpt_oauth_exports);
|
|
27
|
+
var import_node_crypto = require("node:crypto");
|
|
28
|
+
var import_node_http = require("node:http");
|
|
29
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
30
|
+
const ISSUER = "https://auth.openai.com";
|
|
31
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
32
|
+
const REDIRECT_HOST = "localhost";
|
|
33
|
+
const CALLBACK_PORT = 1455;
|
|
34
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
35
|
+
const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
36
|
+
const DUMMY_API_KEY = "dexto-chatgpt-oauth";
|
|
37
|
+
const DEFAULT_CODEX_INSTRUCTIONS = "You are a helpful assistant.";
|
|
38
|
+
const CHATGPT_OAUTH_REDIRECT_URI = `http://${REDIRECT_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
39
|
+
function base64Url(input) {
|
|
40
|
+
return input.toString("base64url");
|
|
41
|
+
}
|
|
42
|
+
function generatePkce() {
|
|
43
|
+
const verifier = base64Url((0, import_node_crypto.randomBytes)(48));
|
|
44
|
+
const challenge = base64Url((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
|
|
45
|
+
return { verifier, challenge };
|
|
46
|
+
}
|
|
47
|
+
function buildAuthorizeUrl(redirectUri, challenge, state) {
|
|
48
|
+
const params = new URLSearchParams({
|
|
49
|
+
response_type: "code",
|
|
50
|
+
client_id: CLIENT_ID,
|
|
51
|
+
redirect_uri: redirectUri,
|
|
52
|
+
scope: "openid profile email offline_access",
|
|
53
|
+
code_challenge: challenge,
|
|
54
|
+
code_challenge_method: "S256",
|
|
55
|
+
id_token_add_organizations: "true",
|
|
56
|
+
codex_cli_simplified_flow: "true",
|
|
57
|
+
state,
|
|
58
|
+
originator: "dexto"
|
|
59
|
+
});
|
|
60
|
+
return `${ISSUER}/oauth/authorize?${params.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
function parseJwtClaims(token) {
|
|
63
|
+
if (!token) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const parts = token.split(".");
|
|
67
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(
|
|
72
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function extractAccountId(tokens) {
|
|
79
|
+
const claims = parseJwtClaims(tokens.id_token) ?? parseJwtClaims(tokens.access_token);
|
|
80
|
+
return claims?.chatgpt_account_id ?? claims?.["https://api.openai.com/auth"]?.chatgpt_account_id ?? claims?.organizations?.find((org) => typeof org.id === "string")?.id;
|
|
81
|
+
}
|
|
82
|
+
function credentialFromTokens(tokens, previous) {
|
|
83
|
+
const refreshToken = tokens.refresh_token ?? previous?.refreshToken;
|
|
84
|
+
if (!refreshToken) {
|
|
85
|
+
throw new Error("ChatGPT Login did not return a refresh token");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
type: "oauth",
|
|
89
|
+
issuer: ISSUER,
|
|
90
|
+
refreshToken,
|
|
91
|
+
accessToken: tokens.access_token,
|
|
92
|
+
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
|
|
93
|
+
accountId: extractAccountId(tokens) ?? previous?.accountId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function exchangeCodeForTokens(code, redirectUri, verifier) {
|
|
97
|
+
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
100
|
+
body: new URLSearchParams({
|
|
101
|
+
grant_type: "authorization_code",
|
|
102
|
+
code,
|
|
103
|
+
redirect_uri: redirectUri,
|
|
104
|
+
client_id: CLIENT_ID,
|
|
105
|
+
code_verifier: verifier
|
|
106
|
+
}).toString()
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`ChatGPT token exchange failed: ${response.status}`);
|
|
110
|
+
}
|
|
111
|
+
return await response.json();
|
|
112
|
+
}
|
|
113
|
+
async function refreshChatGPTOAuthCredential(credential) {
|
|
114
|
+
const response = await fetch(`${credential.issuer}/oauth/token`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
117
|
+
body: new URLSearchParams({
|
|
118
|
+
grant_type: "refresh_token",
|
|
119
|
+
refresh_token: credential.refreshToken,
|
|
120
|
+
client_id: CLIENT_ID
|
|
121
|
+
}).toString()
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`ChatGPT token refresh failed: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
return credentialFromTokens(await response.json(), credential);
|
|
127
|
+
}
|
|
128
|
+
async function closeServer(server) {
|
|
129
|
+
if (!server.listening) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await new Promise((resolve, reject) => {
|
|
133
|
+
server.close((error) => {
|
|
134
|
+
if (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolve();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function listen(server) {
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
server.once("error", reject);
|
|
145
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
146
|
+
server.off("error", reject);
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function startChatGPTBrowserLogin() {
|
|
152
|
+
const redirectUri = CHATGPT_OAUTH_REDIRECT_URI;
|
|
153
|
+
const pkce = generatePkce();
|
|
154
|
+
const state = base64Url((0, import_node_crypto.randomBytes)(32));
|
|
155
|
+
let settled = false;
|
|
156
|
+
let rejectCredential = () => void 0;
|
|
157
|
+
const server = (0, import_node_http.createServer)();
|
|
158
|
+
const credentialPromise = new Promise((resolve, reject) => {
|
|
159
|
+
rejectCredential = reject;
|
|
160
|
+
server.on("request", async (request, response) => {
|
|
161
|
+
try {
|
|
162
|
+
const url = new URL(request.url ?? "/", redirectUri);
|
|
163
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
164
|
+
response.writeHead(404).end("Not found");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (url.searchParams.get("state") !== state) {
|
|
168
|
+
throw new Error("ChatGPT Login returned an invalid state");
|
|
169
|
+
}
|
|
170
|
+
const code = url.searchParams.get("code");
|
|
171
|
+
if (!code) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
url.searchParams.get("error") ?? "ChatGPT Login returned no code"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const tokens = await exchangeCodeForTokens(code, redirectUri, pkce.verifier);
|
|
177
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(
|
|
178
|
+
"<!doctype html><title>Dexto ChatGPT Login</title><p>Authorization complete. You can close this window.</p>"
|
|
179
|
+
);
|
|
180
|
+
settled = true;
|
|
181
|
+
resolve(credentialFromTokens(tokens));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }).end(error instanceof Error ? error.message : "ChatGPT Login failed");
|
|
184
|
+
settled = true;
|
|
185
|
+
reject(error);
|
|
186
|
+
} finally {
|
|
187
|
+
void closeServer(server);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
server.once("error", (error) => {
|
|
191
|
+
settled = true;
|
|
192
|
+
reject(error);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
credentialPromise.catch(() => void 0);
|
|
196
|
+
await listen(server);
|
|
197
|
+
return {
|
|
198
|
+
authUrl: buildAuthorizeUrl(redirectUri, pkce.challenge, state),
|
|
199
|
+
waitForCredential: () => credentialPromise,
|
|
200
|
+
cancel: async () => {
|
|
201
|
+
if (!settled) {
|
|
202
|
+
settled = true;
|
|
203
|
+
rejectCredential(new Error("ChatGPT Login cancelled"));
|
|
204
|
+
}
|
|
205
|
+
await closeServer(server);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function removeAuthorizationHeader(headers) {
|
|
210
|
+
headers.delete("authorization");
|
|
211
|
+
headers.delete("Authorization");
|
|
212
|
+
}
|
|
213
|
+
function resolveCodexEndpoint(requestInput) {
|
|
214
|
+
const url = requestInput instanceof URL ? requestInput : new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
|
|
215
|
+
if (url.pathname.endsWith("/responses") || url.pathname.endsWith("/chat/completions")) {
|
|
216
|
+
return `${CODEX_API_BASE_URL}/responses`;
|
|
217
|
+
}
|
|
218
|
+
return requestInput;
|
|
219
|
+
}
|
|
220
|
+
function contentToInstructionText(content) {
|
|
221
|
+
if (typeof content === "string") {
|
|
222
|
+
return content;
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(content)) {
|
|
225
|
+
const text = content.map((part) => {
|
|
226
|
+
if (typeof part === "string") {
|
|
227
|
+
return part;
|
|
228
|
+
}
|
|
229
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
230
|
+
return part.text;
|
|
231
|
+
}
|
|
232
|
+
return JSON.stringify(part);
|
|
233
|
+
}).filter((part) => part.trim() !== "").join("\n");
|
|
234
|
+
return text === "" ? null : text;
|
|
235
|
+
}
|
|
236
|
+
if (content === void 0 || content === null) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return JSON.stringify(content);
|
|
240
|
+
}
|
|
241
|
+
function isInstructionMessage(message) {
|
|
242
|
+
return !!message && typeof message === "object" && "role" in message && (message.role === "system" || message.role === "developer");
|
|
243
|
+
}
|
|
244
|
+
function extractInstructionMessages(messages) {
|
|
245
|
+
if (!Array.isArray(messages)) {
|
|
246
|
+
return { instructions: [], messages };
|
|
247
|
+
}
|
|
248
|
+
const instructions = [];
|
|
249
|
+
const remaining = [];
|
|
250
|
+
for (const message of messages) {
|
|
251
|
+
if (!isInstructionMessage(message)) {
|
|
252
|
+
remaining.push(message);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const text = contentToInstructionText(message.content);
|
|
256
|
+
if (text) {
|
|
257
|
+
instructions.push(text);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { instructions, messages: remaining };
|
|
261
|
+
}
|
|
262
|
+
function normalizeCodexRequestBody(body) {
|
|
263
|
+
const input = extractInstructionMessages(body.input);
|
|
264
|
+
const messages = extractInstructionMessages(body.messages);
|
|
265
|
+
const instructions = [
|
|
266
|
+
...input.instructions,
|
|
267
|
+
...messages.instructions,
|
|
268
|
+
...typeof body.instructions === "string" && body.instructions.trim() !== "" ? [body.instructions] : []
|
|
269
|
+
];
|
|
270
|
+
if (input.instructions.length > 0) {
|
|
271
|
+
body.input = input.messages;
|
|
272
|
+
}
|
|
273
|
+
if (messages.instructions.length > 0) {
|
|
274
|
+
body.messages = messages.messages;
|
|
275
|
+
}
|
|
276
|
+
body.instructions = instructions.length > 0 ? instructions.join("\n\n") : DEFAULT_CODEX_INSTRUCTIONS;
|
|
277
|
+
body.store = false;
|
|
278
|
+
return body;
|
|
279
|
+
}
|
|
280
|
+
function createCodexRequestInit(init, credential) {
|
|
281
|
+
const headers = new globalThis.Headers(init?.headers);
|
|
282
|
+
removeAuthorizationHeader(headers);
|
|
283
|
+
headers.set("authorization", `Bearer ${credential.accessToken}`);
|
|
284
|
+
headers.set("originator", "dexto");
|
|
285
|
+
headers.set("OpenAI-Beta", "responses=experimental");
|
|
286
|
+
if (credential.accountId) {
|
|
287
|
+
headers.set("ChatGPT-Account-Id", credential.accountId);
|
|
288
|
+
}
|
|
289
|
+
if (typeof init?.body !== "string") {
|
|
290
|
+
return { ...init, headers };
|
|
291
|
+
}
|
|
292
|
+
const body = JSON.parse(init.body);
|
|
293
|
+
return {
|
|
294
|
+
...init,
|
|
295
|
+
body: JSON.stringify(normalizeCodexRequestBody(body)),
|
|
296
|
+
headers
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function createChatGPTRuntimeAuth(input) {
|
|
300
|
+
let current = input.credential;
|
|
301
|
+
let refreshPromise = null;
|
|
302
|
+
async function getFreshCredential() {
|
|
303
|
+
if (current.expiresAt > Date.now() + 3e4) {
|
|
304
|
+
return current;
|
|
305
|
+
}
|
|
306
|
+
refreshPromise ??= refreshChatGPTOAuthCredential(current).then(async (next) => {
|
|
307
|
+
current = next;
|
|
308
|
+
await input.updateCredential(next);
|
|
309
|
+
return next;
|
|
310
|
+
}).finally(() => {
|
|
311
|
+
refreshPromise = null;
|
|
312
|
+
});
|
|
313
|
+
return refreshPromise;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
apiKey: DUMMY_API_KEY,
|
|
317
|
+
baseURL: CODEX_API_BASE_URL,
|
|
318
|
+
fetch: async (requestInput, init) => {
|
|
319
|
+
const credential = await getFreshCredential();
|
|
320
|
+
return fetch(resolveCodexEndpoint(requestInput), {
|
|
321
|
+
...createCodexRequestInit(init, credential)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
327
|
+
0 && (module.exports = {
|
|
328
|
+
CHATGPT_OAUTH_REDIRECT_URI,
|
|
329
|
+
createChatGPTRuntimeAuth,
|
|
330
|
+
refreshChatGPTOAuthCredential,
|
|
331
|
+
startChatGPTBrowserLogin
|
|
332
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const ISSUER = "https://auth.openai.com";
|
|
2
|
+
export declare const CHATGPT_OAUTH_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
3
|
+
export type ChatGPTOAuthCredential = {
|
|
4
|
+
type: 'oauth';
|
|
5
|
+
issuer: typeof ISSUER;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
accessToken: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
accountId?: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
export type PendingChatGPTLogin = {
|
|
12
|
+
authUrl: string;
|
|
13
|
+
waitForCredential(): Promise<ChatGPTOAuthCredential>;
|
|
14
|
+
cancel(): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export type ChatGPTRuntimeAuth = {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
baseURL: string;
|
|
19
|
+
fetch: typeof fetch;
|
|
20
|
+
};
|
|
21
|
+
export declare function refreshChatGPTOAuthCredential(credential: ChatGPTOAuthCredential): Promise<ChatGPTOAuthCredential>;
|
|
22
|
+
export declare function startChatGPTBrowserLogin(): Promise<PendingChatGPTLogin>;
|
|
23
|
+
export declare function createChatGPTRuntimeAuth(input: {
|
|
24
|
+
credential: ChatGPTOAuthCredential;
|
|
25
|
+
updateCredential(credential: ChatGPTOAuthCredential): Promise<void>;
|
|
26
|
+
}): ChatGPTRuntimeAuth;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=chatgpt-oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-oauth.d.ts","sourceRoot":"","sources":["../../src/auth/chatgpt-oauth.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4BAA4B,CAAC;AAQzC,eAAO,MAAM,0BAA0B,wCAA6D,CAAC;AAiBrG,MAAM,MAAM,sBAAsB,GAAG;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACrD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,KAAK,CAAC;CACvB,CAAC;AAiGF,wBAAsB,6BAA6B,CAC/C,UAAU,EAAE,sBAAsB,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAcjC;AA4BD,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAmE7E;AAuID,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC5C,UAAU,EAAE,sBAAsB,CAAC;IACnC,gBAAgB,CAAC,UAAU,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,GAAG,kBAAkB,CAgCrB"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
4
|
+
const ISSUER = "https://auth.openai.com";
|
|
5
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
6
|
+
const REDIRECT_HOST = "localhost";
|
|
7
|
+
const CALLBACK_PORT = 1455;
|
|
8
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
9
|
+
const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
10
|
+
const DUMMY_API_KEY = "dexto-chatgpt-oauth";
|
|
11
|
+
const DEFAULT_CODEX_INSTRUCTIONS = "You are a helpful assistant.";
|
|
12
|
+
const CHATGPT_OAUTH_REDIRECT_URI = `http://${REDIRECT_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
13
|
+
function base64Url(input) {
|
|
14
|
+
return input.toString("base64url");
|
|
15
|
+
}
|
|
16
|
+
function generatePkce() {
|
|
17
|
+
const verifier = base64Url(randomBytes(48));
|
|
18
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
19
|
+
return { verifier, challenge };
|
|
20
|
+
}
|
|
21
|
+
function buildAuthorizeUrl(redirectUri, challenge, state) {
|
|
22
|
+
const params = new URLSearchParams({
|
|
23
|
+
response_type: "code",
|
|
24
|
+
client_id: CLIENT_ID,
|
|
25
|
+
redirect_uri: redirectUri,
|
|
26
|
+
scope: "openid profile email offline_access",
|
|
27
|
+
code_challenge: challenge,
|
|
28
|
+
code_challenge_method: "S256",
|
|
29
|
+
id_token_add_organizations: "true",
|
|
30
|
+
codex_cli_simplified_flow: "true",
|
|
31
|
+
state,
|
|
32
|
+
originator: "dexto"
|
|
33
|
+
});
|
|
34
|
+
return `${ISSUER}/oauth/authorize?${params.toString()}`;
|
|
35
|
+
}
|
|
36
|
+
function parseJwtClaims(token) {
|
|
37
|
+
if (!token) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const parts = token.split(".");
|
|
41
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(
|
|
46
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function extractAccountId(tokens) {
|
|
53
|
+
const claims = parseJwtClaims(tokens.id_token) ?? parseJwtClaims(tokens.access_token);
|
|
54
|
+
return claims?.chatgpt_account_id ?? claims?.["https://api.openai.com/auth"]?.chatgpt_account_id ?? claims?.organizations?.find((org) => typeof org.id === "string")?.id;
|
|
55
|
+
}
|
|
56
|
+
function credentialFromTokens(tokens, previous) {
|
|
57
|
+
const refreshToken = tokens.refresh_token ?? previous?.refreshToken;
|
|
58
|
+
if (!refreshToken) {
|
|
59
|
+
throw new Error("ChatGPT Login did not return a refresh token");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
type: "oauth",
|
|
63
|
+
issuer: ISSUER,
|
|
64
|
+
refreshToken,
|
|
65
|
+
accessToken: tokens.access_token,
|
|
66
|
+
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
|
|
67
|
+
accountId: extractAccountId(tokens) ?? previous?.accountId
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function exchangeCodeForTokens(code, redirectUri, verifier) {
|
|
71
|
+
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
74
|
+
body: new URLSearchParams({
|
|
75
|
+
grant_type: "authorization_code",
|
|
76
|
+
code,
|
|
77
|
+
redirect_uri: redirectUri,
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
code_verifier: verifier
|
|
80
|
+
}).toString()
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`ChatGPT token exchange failed: ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
return await response.json();
|
|
86
|
+
}
|
|
87
|
+
async function refreshChatGPTOAuthCredential(credential) {
|
|
88
|
+
const response = await fetch(`${credential.issuer}/oauth/token`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
91
|
+
body: new URLSearchParams({
|
|
92
|
+
grant_type: "refresh_token",
|
|
93
|
+
refresh_token: credential.refreshToken,
|
|
94
|
+
client_id: CLIENT_ID
|
|
95
|
+
}).toString()
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`ChatGPT token refresh failed: ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
return credentialFromTokens(await response.json(), credential);
|
|
101
|
+
}
|
|
102
|
+
async function closeServer(server) {
|
|
103
|
+
if (!server.listening) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
server.close((error) => {
|
|
108
|
+
if (error) {
|
|
109
|
+
reject(error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function listen(server) {
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
server.once("error", reject);
|
|
119
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
120
|
+
server.off("error", reject);
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function startChatGPTBrowserLogin() {
|
|
126
|
+
const redirectUri = CHATGPT_OAUTH_REDIRECT_URI;
|
|
127
|
+
const pkce = generatePkce();
|
|
128
|
+
const state = base64Url(randomBytes(32));
|
|
129
|
+
let settled = false;
|
|
130
|
+
let rejectCredential = () => void 0;
|
|
131
|
+
const server = createServer();
|
|
132
|
+
const credentialPromise = new Promise((resolve, reject) => {
|
|
133
|
+
rejectCredential = reject;
|
|
134
|
+
server.on("request", async (request, response) => {
|
|
135
|
+
try {
|
|
136
|
+
const url = new URL(request.url ?? "/", redirectUri);
|
|
137
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
138
|
+
response.writeHead(404).end("Not found");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (url.searchParams.get("state") !== state) {
|
|
142
|
+
throw new Error("ChatGPT Login returned an invalid state");
|
|
143
|
+
}
|
|
144
|
+
const code = url.searchParams.get("code");
|
|
145
|
+
if (!code) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
url.searchParams.get("error") ?? "ChatGPT Login returned no code"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const tokens = await exchangeCodeForTokens(code, redirectUri, pkce.verifier);
|
|
151
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(
|
|
152
|
+
"<!doctype html><title>Dexto ChatGPT Login</title><p>Authorization complete. You can close this window.</p>"
|
|
153
|
+
);
|
|
154
|
+
settled = true;
|
|
155
|
+
resolve(credentialFromTokens(tokens));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }).end(error instanceof Error ? error.message : "ChatGPT Login failed");
|
|
158
|
+
settled = true;
|
|
159
|
+
reject(error);
|
|
160
|
+
} finally {
|
|
161
|
+
void closeServer(server);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
server.once("error", (error) => {
|
|
165
|
+
settled = true;
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
credentialPromise.catch(() => void 0);
|
|
170
|
+
await listen(server);
|
|
171
|
+
return {
|
|
172
|
+
authUrl: buildAuthorizeUrl(redirectUri, pkce.challenge, state),
|
|
173
|
+
waitForCredential: () => credentialPromise,
|
|
174
|
+
cancel: async () => {
|
|
175
|
+
if (!settled) {
|
|
176
|
+
settled = true;
|
|
177
|
+
rejectCredential(new Error("ChatGPT Login cancelled"));
|
|
178
|
+
}
|
|
179
|
+
await closeServer(server);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function removeAuthorizationHeader(headers) {
|
|
184
|
+
headers.delete("authorization");
|
|
185
|
+
headers.delete("Authorization");
|
|
186
|
+
}
|
|
187
|
+
function resolveCodexEndpoint(requestInput) {
|
|
188
|
+
const url = requestInput instanceof URL ? requestInput : new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
|
|
189
|
+
if (url.pathname.endsWith("/responses") || url.pathname.endsWith("/chat/completions")) {
|
|
190
|
+
return `${CODEX_API_BASE_URL}/responses`;
|
|
191
|
+
}
|
|
192
|
+
return requestInput;
|
|
193
|
+
}
|
|
194
|
+
function contentToInstructionText(content) {
|
|
195
|
+
if (typeof content === "string") {
|
|
196
|
+
return content;
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(content)) {
|
|
199
|
+
const text = content.map((part) => {
|
|
200
|
+
if (typeof part === "string") {
|
|
201
|
+
return part;
|
|
202
|
+
}
|
|
203
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
204
|
+
return part.text;
|
|
205
|
+
}
|
|
206
|
+
return JSON.stringify(part);
|
|
207
|
+
}).filter((part) => part.trim() !== "").join("\n");
|
|
208
|
+
return text === "" ? null : text;
|
|
209
|
+
}
|
|
210
|
+
if (content === void 0 || content === null) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(content);
|
|
214
|
+
}
|
|
215
|
+
function isInstructionMessage(message) {
|
|
216
|
+
return !!message && typeof message === "object" && "role" in message && (message.role === "system" || message.role === "developer");
|
|
217
|
+
}
|
|
218
|
+
function extractInstructionMessages(messages) {
|
|
219
|
+
if (!Array.isArray(messages)) {
|
|
220
|
+
return { instructions: [], messages };
|
|
221
|
+
}
|
|
222
|
+
const instructions = [];
|
|
223
|
+
const remaining = [];
|
|
224
|
+
for (const message of messages) {
|
|
225
|
+
if (!isInstructionMessage(message)) {
|
|
226
|
+
remaining.push(message);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const text = contentToInstructionText(message.content);
|
|
230
|
+
if (text) {
|
|
231
|
+
instructions.push(text);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { instructions, messages: remaining };
|
|
235
|
+
}
|
|
236
|
+
function normalizeCodexRequestBody(body) {
|
|
237
|
+
const input = extractInstructionMessages(body.input);
|
|
238
|
+
const messages = extractInstructionMessages(body.messages);
|
|
239
|
+
const instructions = [
|
|
240
|
+
...input.instructions,
|
|
241
|
+
...messages.instructions,
|
|
242
|
+
...typeof body.instructions === "string" && body.instructions.trim() !== "" ? [body.instructions] : []
|
|
243
|
+
];
|
|
244
|
+
if (input.instructions.length > 0) {
|
|
245
|
+
body.input = input.messages;
|
|
246
|
+
}
|
|
247
|
+
if (messages.instructions.length > 0) {
|
|
248
|
+
body.messages = messages.messages;
|
|
249
|
+
}
|
|
250
|
+
body.instructions = instructions.length > 0 ? instructions.join("\n\n") : DEFAULT_CODEX_INSTRUCTIONS;
|
|
251
|
+
body.store = false;
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
function createCodexRequestInit(init, credential) {
|
|
255
|
+
const headers = new globalThis.Headers(init?.headers);
|
|
256
|
+
removeAuthorizationHeader(headers);
|
|
257
|
+
headers.set("authorization", `Bearer ${credential.accessToken}`);
|
|
258
|
+
headers.set("originator", "dexto");
|
|
259
|
+
headers.set("OpenAI-Beta", "responses=experimental");
|
|
260
|
+
if (credential.accountId) {
|
|
261
|
+
headers.set("ChatGPT-Account-Id", credential.accountId);
|
|
262
|
+
}
|
|
263
|
+
if (typeof init?.body !== "string") {
|
|
264
|
+
return { ...init, headers };
|
|
265
|
+
}
|
|
266
|
+
const body = JSON.parse(init.body);
|
|
267
|
+
return {
|
|
268
|
+
...init,
|
|
269
|
+
body: JSON.stringify(normalizeCodexRequestBody(body)),
|
|
270
|
+
headers
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function createChatGPTRuntimeAuth(input) {
|
|
274
|
+
let current = input.credential;
|
|
275
|
+
let refreshPromise = null;
|
|
276
|
+
async function getFreshCredential() {
|
|
277
|
+
if (current.expiresAt > Date.now() + 3e4) {
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
refreshPromise ??= refreshChatGPTOAuthCredential(current).then(async (next) => {
|
|
281
|
+
current = next;
|
|
282
|
+
await input.updateCredential(next);
|
|
283
|
+
return next;
|
|
284
|
+
}).finally(() => {
|
|
285
|
+
refreshPromise = null;
|
|
286
|
+
});
|
|
287
|
+
return refreshPromise;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
apiKey: DUMMY_API_KEY,
|
|
291
|
+
baseURL: CODEX_API_BASE_URL,
|
|
292
|
+
fetch: async (requestInput, init) => {
|
|
293
|
+
const credential = await getFreshCredential();
|
|
294
|
+
return fetch(resolveCodexEndpoint(requestInput), {
|
|
295
|
+
...createCodexRequestInit(init, credential)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
CHATGPT_OAUTH_REDIRECT_URI,
|
|
302
|
+
createChatGPTRuntimeAuth,
|
|
303
|
+
refreshChatGPTOAuthCredential,
|
|
304
|
+
startChatGPTBrowserLogin
|
|
305
|
+
};
|