@oh-my-pi/pi-ai 6.7.670 → 6.8.0
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 +31 -31
- package/package.json +2 -1
- package/src/cli.ts +114 -52
- package/src/providers/google-gemini-cli.ts +4 -20
- package/src/providers/openai-codex/response-handler.ts +4 -43
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/storage.ts +185 -0
- package/src/utils/event-stream.ts +3 -3
- package/src/utils/oauth/anthropic.ts +68 -97
- package/src/utils/oauth/callback-server.ts +245 -0
- package/src/utils/oauth/cursor.ts +1 -5
- package/src/utils/oauth/github-copilot.ts +1 -23
- package/src/utils/oauth/google-antigravity.ts +73 -263
- package/src/utils/oauth/google-gemini-cli.ts +73 -281
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/openai-codex.ts +131 -318
- package/src/utils/oauth/types.ts +8 -0
|
@@ -2,34 +2,18 @@
|
|
|
2
2
|
* OpenAI Codex (ChatGPT OAuth) flow
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import http from "node:http";
|
|
5
|
+
import { OAuthCallbackFlow, parseCallbackInput } from "./callback-server";
|
|
7
6
|
import { generatePKCE } from "./pkce";
|
|
8
|
-
import type {
|
|
7
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
9
8
|
|
|
10
9
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
11
10
|
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
12
11
|
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
13
|
-
const
|
|
12
|
+
const CALLBACK_PORT = 1455;
|
|
13
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
14
14
|
const SCOPE = "openid profile email offline_access";
|
|
15
15
|
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
16
16
|
|
|
17
|
-
const SUCCESS_HTML = `<!doctype html>
|
|
18
|
-
<html lang="en">
|
|
19
|
-
<head>
|
|
20
|
-
<meta charset="utf-8" />
|
|
21
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
-
<title>Authentication successful</title>
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
<p>Authentication successful. Return to your terminal to continue.</p>
|
|
26
|
-
</body>
|
|
27
|
-
</html>`;
|
|
28
|
-
|
|
29
|
-
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
|
|
30
|
-
type TokenFailure = { type: "failed" };
|
|
31
|
-
type TokenResult = TokenSuccess | TokenFailure;
|
|
32
|
-
|
|
33
17
|
type JwtPayload = {
|
|
34
18
|
[JWT_CLAIM_PATH]?: {
|
|
35
19
|
chatgpt_account_id?: string;
|
|
@@ -37,40 +21,6 @@ type JwtPayload = {
|
|
|
37
21
|
[key: string]: unknown;
|
|
38
22
|
};
|
|
39
23
|
|
|
40
|
-
function createState(): string {
|
|
41
|
-
return randomBytes(16).toString("hex");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseAuthorizationInput(input: string): { code?: string; state?: string } {
|
|
45
|
-
const value = input.trim();
|
|
46
|
-
if (!value) return {};
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const url = new URL(value);
|
|
50
|
-
return {
|
|
51
|
-
code: url.searchParams.get("code") ?? undefined,
|
|
52
|
-
state: url.searchParams.get("state") ?? undefined,
|
|
53
|
-
};
|
|
54
|
-
} catch {
|
|
55
|
-
// not a URL
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (value.includes("#")) {
|
|
59
|
-
const [code, state] = value.split("#", 2);
|
|
60
|
-
return { code, state };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (value.includes("code=")) {
|
|
64
|
-
const params = new URLSearchParams(value);
|
|
65
|
-
return {
|
|
66
|
-
code: params.get("code") ?? undefined,
|
|
67
|
-
state: params.get("state") ?? undefined,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { code: value };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
24
|
function decodeJwt(token: string): JwtPayload | null {
|
|
75
25
|
try {
|
|
76
26
|
const parts = token.split(".");
|
|
@@ -83,306 +33,153 @@ function decodeJwt(token: string): JwtPayload | null {
|
|
|
83
33
|
}
|
|
84
34
|
}
|
|
85
35
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
94
|
-
body: new URLSearchParams({
|
|
95
|
-
grant_type: "authorization_code",
|
|
96
|
-
client_id: CLIENT_ID,
|
|
97
|
-
code,
|
|
98
|
-
code_verifier: verifier,
|
|
99
|
-
redirect_uri: redirectUri,
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
36
|
+
function getAccountId(accessToken: string): string | null {
|
|
37
|
+
const payload = decodeJwt(accessToken);
|
|
38
|
+
const auth = payload?.[JWT_CLAIM_PATH];
|
|
39
|
+
const accountId = auth?.chatgpt_account_id;
|
|
40
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
41
|
+
}
|
|
102
42
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
43
|
+
class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
44
|
+
private verifier: string = "";
|
|
45
|
+
private challenge: string = "";
|
|
46
|
+
|
|
47
|
+
constructor(ctrl: OAuthController) {
|
|
48
|
+
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
107
49
|
}
|
|
108
50
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
51
|
+
protected async generateAuthUrl(
|
|
52
|
+
state: string,
|
|
53
|
+
redirectUri: string,
|
|
54
|
+
): Promise<{ url: string; instructions?: string }> {
|
|
55
|
+
const pkce = await generatePKCE();
|
|
56
|
+
this.verifier = pkce.verifier;
|
|
57
|
+
this.challenge = pkce.challenge;
|
|
114
58
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
59
|
+
const searchParams = new URLSearchParams({
|
|
60
|
+
response_type: "code",
|
|
61
|
+
client_id: CLIENT_ID,
|
|
62
|
+
redirect_uri: redirectUri,
|
|
63
|
+
scope: SCOPE,
|
|
64
|
+
code_challenge: this.challenge,
|
|
65
|
+
code_challenge_method: "S256",
|
|
66
|
+
state,
|
|
67
|
+
id_token_add_organizations: "true",
|
|
68
|
+
codex_cli_simplified_flow: "true",
|
|
69
|
+
originator: "opencode",
|
|
70
|
+
});
|
|
119
71
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
refresh: json.refresh_token,
|
|
124
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
72
|
+
const url = `${AUTHORIZE_URL}?${searchParams.toString()}`;
|
|
73
|
+
return { url, instructions: "A browser window should open. Complete login to finish." };
|
|
74
|
+
}
|
|
127
75
|
|
|
128
|
-
async
|
|
129
|
-
|
|
130
|
-
const response = await fetch(TOKEN_URL, {
|
|
76
|
+
protected async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
77
|
+
const tokenResponse = await fetch(TOKEN_URL, {
|
|
131
78
|
method: "POST",
|
|
132
79
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
133
80
|
body: new URLSearchParams({
|
|
134
|
-
grant_type: "
|
|
135
|
-
refresh_token: refreshToken,
|
|
81
|
+
grant_type: "authorization_code",
|
|
136
82
|
client_id: CLIENT_ID,
|
|
83
|
+
code,
|
|
84
|
+
code_verifier: this.verifier,
|
|
85
|
+
redirect_uri: redirectUri,
|
|
137
86
|
}),
|
|
138
87
|
});
|
|
139
88
|
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
console.error("[openai-codex] Token refresh failed:", response.status, text);
|
|
143
|
-
return { type: "failed" };
|
|
89
|
+
if (!tokenResponse.ok) {
|
|
90
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
144
91
|
}
|
|
145
92
|
|
|
146
|
-
const
|
|
93
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
147
94
|
access_token?: string;
|
|
148
95
|
refresh_token?: string;
|
|
149
96
|
expires_in?: number;
|
|
150
97
|
};
|
|
151
98
|
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
|
|
99
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
|
|
100
|
+
throw new Error("Token response missing required fields");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const accountId = getAccountId(tokenData.access_token);
|
|
104
|
+
if (!accountId) {
|
|
105
|
+
throw new Error("Failed to extract accountId from token");
|
|
155
106
|
}
|
|
156
107
|
|
|
157
108
|
return {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
109
|
+
access: tokenData.access_token,
|
|
110
|
+
refresh: tokenData.refresh_token,
|
|
111
|
+
expires: Date.now() + tokenData.expires_in * 1000,
|
|
112
|
+
accountId,
|
|
162
113
|
};
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.error("[openai-codex] Token refresh error:", error);
|
|
165
|
-
return { type: "failed" };
|
|
166
114
|
}
|
|
167
115
|
}
|
|
168
116
|
|
|
169
|
-
async function createAuthorizationFlow(): Promise<{ verifier: string; state: string; url: string }> {
|
|
170
|
-
const { verifier, challenge } = await generatePKCE();
|
|
171
|
-
const state = createState();
|
|
172
|
-
|
|
173
|
-
const url = new URL(AUTHORIZE_URL);
|
|
174
|
-
url.searchParams.set("response_type", "code");
|
|
175
|
-
url.searchParams.set("client_id", CLIENT_ID);
|
|
176
|
-
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
177
|
-
url.searchParams.set("scope", SCOPE);
|
|
178
|
-
url.searchParams.set("code_challenge", challenge);
|
|
179
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
180
|
-
url.searchParams.set("state", state);
|
|
181
|
-
url.searchParams.set("id_token_add_organizations", "true");
|
|
182
|
-
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
183
|
-
url.searchParams.set("originator", "opencode");
|
|
184
|
-
|
|
185
|
-
return { verifier, state, url: url.toString() };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
type OAuthServerInfo = {
|
|
189
|
-
close: () => void;
|
|
190
|
-
cancelWait: () => void;
|
|
191
|
-
waitForCode: () => Promise<{ code: string } | null>;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
|
195
|
-
let lastCode: string | null = null;
|
|
196
|
-
let cancelled = false;
|
|
197
|
-
const server = http.createServer((req, res) => {
|
|
198
|
-
try {
|
|
199
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
200
|
-
if (url.pathname !== "/auth/callback") {
|
|
201
|
-
res.statusCode = 404;
|
|
202
|
-
res.end("Not found");
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (url.searchParams.get("state") !== state) {
|
|
206
|
-
res.statusCode = 400;
|
|
207
|
-
res.end("State mismatch");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const code = url.searchParams.get("code");
|
|
211
|
-
if (!code) {
|
|
212
|
-
res.statusCode = 400;
|
|
213
|
-
res.end("Missing authorization code");
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
res.statusCode = 200;
|
|
217
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
218
|
-
res.end(SUCCESS_HTML);
|
|
219
|
-
lastCode = code;
|
|
220
|
-
} catch {
|
|
221
|
-
res.statusCode = 500;
|
|
222
|
-
res.end("Internal error");
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return new Promise((resolve) => {
|
|
227
|
-
server
|
|
228
|
-
.listen(1455, "127.0.0.1", () => {
|
|
229
|
-
resolve({
|
|
230
|
-
close: () => server.close(),
|
|
231
|
-
cancelWait: () => {
|
|
232
|
-
cancelled = true;
|
|
233
|
-
},
|
|
234
|
-
waitForCode: async () => {
|
|
235
|
-
const sleep = () => new Promise((r) => setTimeout(r, 100));
|
|
236
|
-
for (let i = 0; i < 600; i += 1) {
|
|
237
|
-
if (lastCode) return { code: lastCode };
|
|
238
|
-
if (cancelled) return null;
|
|
239
|
-
await sleep();
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
})
|
|
245
|
-
.on("error", (err: NodeJS.ErrnoException) => {
|
|
246
|
-
console.error(
|
|
247
|
-
"[openai-codex] Failed to bind http://127.0.0.1:1455 (",
|
|
248
|
-
err.code,
|
|
249
|
-
") Falling back to manual paste.",
|
|
250
|
-
);
|
|
251
|
-
resolve({
|
|
252
|
-
close: () => {
|
|
253
|
-
try {
|
|
254
|
-
server.close();
|
|
255
|
-
} catch {
|
|
256
|
-
// ignore
|
|
257
|
-
}
|
|
258
|
-
},
|
|
259
|
-
cancelWait: () => {},
|
|
260
|
-
waitForCode: async () => null,
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function getAccountId(accessToken: string): string | null {
|
|
267
|
-
const payload = decodeJwt(accessToken);
|
|
268
|
-
const auth = payload?.[JWT_CLAIM_PATH];
|
|
269
|
-
const accountId = auth?.chatgpt_account_id;
|
|
270
|
-
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
117
|
/**
|
|
274
118
|
* Login with OpenAI Codex OAuth
|
|
275
|
-
*
|
|
276
|
-
* @param options.onAuth - Called with URL and instructions when auth starts
|
|
277
|
-
* @param options.onPrompt - Called to prompt user for manual code paste (fallback if no onManualCodeInput)
|
|
278
|
-
* @param options.onProgress - Optional progress messages
|
|
279
|
-
* @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
|
|
280
|
-
* Races with browser callback - whichever completes first wins.
|
|
281
|
-
* Useful for showing paste input immediately alongside browser flow.
|
|
282
119
|
*/
|
|
283
|
-
export async function loginOpenAICodex(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
onProgress?: (message: string) => void;
|
|
287
|
-
onManualCodeInput?: () => Promise<string>;
|
|
288
|
-
}): Promise<OAuthCredentials> {
|
|
289
|
-
const { verifier, state, url } = await createAuthorizationFlow();
|
|
290
|
-
const server = await startLocalOAuthServer(state);
|
|
291
|
-
|
|
292
|
-
options.onAuth({ url, instructions: "A browser window should open. Complete login to finish." });
|
|
293
|
-
|
|
294
|
-
let code: string | undefined;
|
|
120
|
+
export async function loginOpenAICodex(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
121
|
+
const flow = new OpenAICodexOAuthFlow(ctrl);
|
|
122
|
+
|
|
295
123
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.onManualCodeInput()
|
|
302
|
-
.then((input) => {
|
|
303
|
-
manualCode = input;
|
|
304
|
-
server.cancelWait();
|
|
305
|
-
})
|
|
306
|
-
.catch((err) => {
|
|
307
|
-
manualError = err instanceof Error ? err : new Error(String(err));
|
|
308
|
-
server.cancelWait();
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const result = await server.waitForCode();
|
|
312
|
-
|
|
313
|
-
// If manual input was cancelled, throw that error
|
|
314
|
-
if (manualError) {
|
|
315
|
-
throw manualError;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (result?.code) {
|
|
319
|
-
// Browser callback won
|
|
320
|
-
code = result.code;
|
|
321
|
-
} else if (manualCode) {
|
|
322
|
-
// Manual input won (or callback timed out and user had entered code)
|
|
323
|
-
const parsed = parseAuthorizationInput(manualCode);
|
|
324
|
-
if (parsed.state && parsed.state !== state) {
|
|
325
|
-
throw new Error("State mismatch");
|
|
326
|
-
}
|
|
327
|
-
code = parsed.code;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// If still no code, wait for manual promise to complete and try that
|
|
331
|
-
if (!code) {
|
|
332
|
-
await manualPromise;
|
|
333
|
-
if (manualError) {
|
|
334
|
-
throw manualError;
|
|
335
|
-
}
|
|
336
|
-
if (manualCode) {
|
|
337
|
-
const parsed = parseAuthorizationInput(manualCode);
|
|
338
|
-
if (parsed.state && parsed.state !== state) {
|
|
339
|
-
throw new Error("State mismatch");
|
|
340
|
-
}
|
|
341
|
-
code = parsed.code;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
// Original flow: wait for callback, then prompt if needed
|
|
346
|
-
const result = await server.waitForCode();
|
|
347
|
-
if (result?.code) {
|
|
348
|
-
code = result.code;
|
|
349
|
-
}
|
|
124
|
+
return await flow.login();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Callback failed - fall back to onPrompt if available
|
|
127
|
+
if (!ctrl.onPrompt) {
|
|
128
|
+
throw error;
|
|
350
129
|
}
|
|
351
130
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
code = parsed.code;
|
|
131
|
+
ctrl.onProgress?.("Callback server failed, falling back to manual input");
|
|
132
|
+
|
|
133
|
+
const input = await ctrl.onPrompt({
|
|
134
|
+
message: "Paste the authorization code (or full redirect URL):",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const parsed = parseCallbackInput(input);
|
|
138
|
+
if (!parsed.code) {
|
|
139
|
+
throw new Error("No authorization code found in input");
|
|
362
140
|
}
|
|
363
141
|
|
|
364
|
-
|
|
365
|
-
|
|
142
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
143
|
+
|
|
144
|
+
// Manual token exchange
|
|
145
|
+
const pkce = await generatePKCE();
|
|
146
|
+
const tokenResponse = await fetch(TOKEN_URL, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
149
|
+
body: new URLSearchParams({
|
|
150
|
+
grant_type: "authorization_code",
|
|
151
|
+
client_id: CLIENT_ID,
|
|
152
|
+
code: parsed.code,
|
|
153
|
+
code_verifier: pkce.verifier,
|
|
154
|
+
redirect_uri: redirectUri,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!tokenResponse.ok) {
|
|
159
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
366
160
|
}
|
|
367
161
|
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
162
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
163
|
+
access_token?: string;
|
|
164
|
+
refresh_token?: string;
|
|
165
|
+
expires_in?: number;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
|
|
169
|
+
throw new Error("Token response missing required fields");
|
|
371
170
|
}
|
|
372
171
|
|
|
373
|
-
const accountId = getAccountId(
|
|
172
|
+
const accountId = getAccountId(tokenData.access_token);
|
|
374
173
|
if (!accountId) {
|
|
375
174
|
throw new Error("Failed to extract accountId from token");
|
|
376
175
|
}
|
|
377
176
|
|
|
378
177
|
return {
|
|
379
|
-
access:
|
|
380
|
-
refresh:
|
|
381
|
-
expires:
|
|
178
|
+
access: tokenData.access_token,
|
|
179
|
+
refresh: tokenData.refresh_token,
|
|
180
|
+
expires: Date.now() + tokenData.expires_in * 1000,
|
|
382
181
|
accountId,
|
|
383
182
|
};
|
|
384
|
-
} finally {
|
|
385
|
-
server.close();
|
|
386
183
|
}
|
|
387
184
|
}
|
|
388
185
|
|
|
@@ -390,20 +187,36 @@ export async function loginOpenAICodex(options: {
|
|
|
390
187
|
* Refresh OpenAI Codex OAuth token
|
|
391
188
|
*/
|
|
392
189
|
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
190
|
+
const response = await fetch(TOKEN_URL, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
193
|
+
body: new URLSearchParams({
|
|
194
|
+
grant_type: "refresh_token",
|
|
195
|
+
refresh_token: refreshToken,
|
|
196
|
+
client_id: CLIENT_ID,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`OpenAI Codex token refresh failed: ${response.status}`);
|
|
396
202
|
}
|
|
397
203
|
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
204
|
+
const tokenData = (await response.json()) as {
|
|
205
|
+
access_token?: string;
|
|
206
|
+
refresh_token?: string;
|
|
207
|
+
expires_in?: number;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
|
|
211
|
+
throw new Error("Token response missing required fields");
|
|
401
212
|
}
|
|
402
213
|
|
|
214
|
+
const accountId = getAccountId(tokenData.access_token);
|
|
215
|
+
|
|
403
216
|
return {
|
|
404
|
-
access:
|
|
405
|
-
refresh:
|
|
406
|
-
expires:
|
|
407
|
-
accountId,
|
|
217
|
+
access: tokenData.access_token,
|
|
218
|
+
refresh: tokenData.refresh_token || refreshToken,
|
|
219
|
+
expires: Date.now() + tokenData.expires_in * 1000,
|
|
220
|
+
accountId: accountId ?? undefined,
|
|
408
221
|
};
|
|
409
222
|
}
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -32,3 +32,11 @@ export interface OAuthProviderInfo {
|
|
|
32
32
|
name: string;
|
|
33
33
|
available: boolean;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
export interface OAuthController {
|
|
37
|
+
onAuth?(info: { url: string; instructions?: string }): void;
|
|
38
|
+
onProgress?(message: string): void;
|
|
39
|
+
onManualCodeInput?(): Promise<string>;
|
|
40
|
+
onPrompt?(prompt: OAuthPrompt): Promise<string>;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|