codex-subscription-oauth-plugin 0.1.1
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/LICENSE +21 -0
- package/README.ja.md +41 -0
- package/README.md +41 -0
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/cordis.patch.yml +14 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +1054 -0
- package/lib/models-DaqoXO5b.js +941 -0
- package/lib/openai-codex-responses-DPxdQtbt.js +2827 -0
- package/lib/openai-codex.js +574 -0
- package/lib/provider-env-DRzE1dvc.js +41 -0
- package/package.json +87 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { t as getProviderEnvValue } from "./provider-env-DRzE1dvc.js";
|
|
2
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/oauth/device-code.js
|
|
3
|
+
const CANCEL_MESSAGE = "Login cancelled";
|
|
4
|
+
const TIMEOUT_MESSAGE = "Device flow timed out";
|
|
5
|
+
const SLOW_DOWN_TIMEOUT_MESSAGE = "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";
|
|
6
|
+
const MINIMUM_INTERVAL_MS = 1e3;
|
|
7
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
8
|
+
const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5e3;
|
|
9
|
+
function abortableSleep(ms, signal, cancelMessage) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (signal.aborted) {
|
|
12
|
+
reject(new Error(cancelMessage));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const onAbort = () => {
|
|
16
|
+
clearTimeout(timeout);
|
|
17
|
+
reject(new Error(cancelMessage));
|
|
18
|
+
};
|
|
19
|
+
const timeout = setTimeout(() => {
|
|
20
|
+
signal.removeEventListener("abort", onAbort);
|
|
21
|
+
resolve();
|
|
22
|
+
}, ms);
|
|
23
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function pollOAuthDeviceCodeFlow(options) {
|
|
27
|
+
const deadline = typeof options.expiresInSeconds === "number" ? Date.now() + options.expiresInSeconds * 1e3 : Number.POSITIVE_INFINITY;
|
|
28
|
+
let intervalMs = Math.max(MINIMUM_INTERVAL_MS, Math.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1e3));
|
|
29
|
+
let slowDownResponses = 0;
|
|
30
|
+
if (options.waitBeforeFirstPoll) {
|
|
31
|
+
const remainingMs = deadline - Date.now();
|
|
32
|
+
if (remainingMs > 0) await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
|
|
33
|
+
}
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
if (options.signal.aborted) throw new Error(CANCEL_MESSAGE);
|
|
36
|
+
const result = await options.poll();
|
|
37
|
+
if (result.status === "complete") return result.value;
|
|
38
|
+
if (result.status === "failed") throw new Error(result.message);
|
|
39
|
+
if (result.status === "slow_down") {
|
|
40
|
+
slowDownResponses += 1;
|
|
41
|
+
intervalMs = typeof result.intervalSeconds === "number" && Number.isFinite(result.intervalSeconds) && result.intervalSeconds > 0 ? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1e3)) : Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
|
|
42
|
+
}
|
|
43
|
+
const remainingMs = deadline - Date.now();
|
|
44
|
+
if (remainingMs <= 0) break;
|
|
45
|
+
await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/oauth/oauth-page.js
|
|
51
|
+
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;
|
|
52
|
+
function escapeHtml(value) {
|
|
53
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
54
|
+
}
|
|
55
|
+
function renderPage(options) {
|
|
56
|
+
const title = escapeHtml(options.title);
|
|
57
|
+
const heading = escapeHtml(options.heading);
|
|
58
|
+
const message = escapeHtml(options.message);
|
|
59
|
+
const details = options.details ? escapeHtml(options.details) : void 0;
|
|
60
|
+
return `<!doctype html>
|
|
61
|
+
<html lang="en">
|
|
62
|
+
<head>
|
|
63
|
+
<meta charset="utf-8" />
|
|
64
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
65
|
+
<title>${title}</title>
|
|
66
|
+
<style>
|
|
67
|
+
:root {
|
|
68
|
+
--text: #fafafa;
|
|
69
|
+
--text-dim: #a1a1aa;
|
|
70
|
+
--page-bg: #09090b;
|
|
71
|
+
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
72
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
73
|
+
}
|
|
74
|
+
* { box-sizing: border-box; }
|
|
75
|
+
html { color-scheme: dark; }
|
|
76
|
+
body {
|
|
77
|
+
margin: 0;
|
|
78
|
+
min-height: 100vh;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
padding: 24px;
|
|
83
|
+
background: var(--page-bg);
|
|
84
|
+
color: var(--text);
|
|
85
|
+
font-family: var(--font-sans);
|
|
86
|
+
text-align: center;
|
|
87
|
+
}
|
|
88
|
+
main {
|
|
89
|
+
width: 100%;
|
|
90
|
+
max-width: 560px;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
}
|
|
96
|
+
.logo {
|
|
97
|
+
width: 72px;
|
|
98
|
+
height: 72px;
|
|
99
|
+
display: block;
|
|
100
|
+
margin-bottom: 24px;
|
|
101
|
+
}
|
|
102
|
+
h1 {
|
|
103
|
+
margin: 0 0 10px;
|
|
104
|
+
font-size: 28px;
|
|
105
|
+
line-height: 1.15;
|
|
106
|
+
font-weight: 650;
|
|
107
|
+
color: var(--text);
|
|
108
|
+
}
|
|
109
|
+
p {
|
|
110
|
+
margin: 0;
|
|
111
|
+
line-height: 1.7;
|
|
112
|
+
color: var(--text-dim);
|
|
113
|
+
font-size: 15px;
|
|
114
|
+
}
|
|
115
|
+
.details {
|
|
116
|
+
margin-top: 16px;
|
|
117
|
+
font-family: var(--font-mono);
|
|
118
|
+
font-size: 13px;
|
|
119
|
+
color: var(--text-dim);
|
|
120
|
+
white-space: pre-wrap;
|
|
121
|
+
word-break: break-word;
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
124
|
+
</head>
|
|
125
|
+
<body>
|
|
126
|
+
<main>
|
|
127
|
+
<div class="logo">${LOGO_SVG}</div>
|
|
128
|
+
<h1>${heading}</h1>
|
|
129
|
+
<p>${message}</p>
|
|
130
|
+
${details ? `<div class="details">${details}</div>` : ""}
|
|
131
|
+
</main>
|
|
132
|
+
</body>
|
|
133
|
+
</html>`;
|
|
134
|
+
}
|
|
135
|
+
function oauthSuccessHtml(message) {
|
|
136
|
+
return renderPage({
|
|
137
|
+
title: "Authentication successful",
|
|
138
|
+
heading: "Authentication successful",
|
|
139
|
+
message
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function oauthErrorHtml(message, details) {
|
|
143
|
+
return renderPage({
|
|
144
|
+
title: "Authentication failed",
|
|
145
|
+
heading: "Authentication failed",
|
|
146
|
+
message,
|
|
147
|
+
details
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/oauth/pkce.js
|
|
152
|
+
/**
|
|
153
|
+
* PKCE utilities using Web Crypto API.
|
|
154
|
+
* Works in both Node.js 20+ and browsers.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Encode bytes as base64url string.
|
|
158
|
+
*/
|
|
159
|
+
function base64urlEncode(bytes) {
|
|
160
|
+
let binary = "";
|
|
161
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
162
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Generate PKCE code verifier and challenge.
|
|
166
|
+
* Uses Web Crypto API for cross-platform compatibility.
|
|
167
|
+
*/
|
|
168
|
+
async function generatePKCE() {
|
|
169
|
+
const verifierBytes = /* @__PURE__ */ new Uint8Array(32);
|
|
170
|
+
crypto.getRandomValues(verifierBytes);
|
|
171
|
+
const verifier = base64urlEncode(verifierBytes);
|
|
172
|
+
const data = new TextEncoder().encode(verifier);
|
|
173
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
174
|
+
return {
|
|
175
|
+
verifier,
|
|
176
|
+
challenge: base64urlEncode(new Uint8Array(hashBuffer))
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/oauth/openai-codex.js
|
|
181
|
+
/**
|
|
182
|
+
* OpenAI Codex (ChatGPT OAuth) flow
|
|
183
|
+
*
|
|
184
|
+
* NOTE: This module uses Node.js crypto and http for the OAuth callback.
|
|
185
|
+
* It is only intended for CLI use, not browser environments.
|
|
186
|
+
*/
|
|
187
|
+
let _randomBytes = null;
|
|
188
|
+
let _http = null;
|
|
189
|
+
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
190
|
+
import("node:crypto").then((m) => {
|
|
191
|
+
_randomBytes = m.randomBytes;
|
|
192
|
+
});
|
|
193
|
+
import("node:http").then((m) => {
|
|
194
|
+
_http = m;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
198
|
+
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
199
|
+
const AUTHORIZE_URL = `${AUTH_BASE_URL}/oauth/authorize`;
|
|
200
|
+
const TOKEN_URL = `${AUTH_BASE_URL}/oauth/token`;
|
|
201
|
+
const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
202
|
+
const DEVICE_USER_CODE_URL = `${AUTH_BASE_URL}/api/accounts/deviceauth/usercode`;
|
|
203
|
+
const DEVICE_TOKEN_URL = `${AUTH_BASE_URL}/api/accounts/deviceauth/token`;
|
|
204
|
+
const DEVICE_VERIFICATION_URI = `${AUTH_BASE_URL}/codex/device`;
|
|
205
|
+
const DEVICE_REDIRECT_URI = `${AUTH_BASE_URL}/deviceauth/callback`;
|
|
206
|
+
const DEVICE_CODE_TIMEOUT_SECONDS = 900;
|
|
207
|
+
const OPENAI_CODEX_BROWSER_LOGIN_METHOD = "browser";
|
|
208
|
+
const OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD = "device_code";
|
|
209
|
+
const SCOPE = "openid profile email offline_access";
|
|
210
|
+
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
211
|
+
function getCallbackHost() {
|
|
212
|
+
return getProviderEnvValue("PI_OAUTH_CALLBACK_HOST") || "127.0.0.1";
|
|
213
|
+
}
|
|
214
|
+
function createState() {
|
|
215
|
+
if (!_randomBytes) throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
|
|
216
|
+
return _randomBytes(16).toString("hex");
|
|
217
|
+
}
|
|
218
|
+
function parseAuthorizationInput(input) {
|
|
219
|
+
const value = input.trim();
|
|
220
|
+
if (!value) return {};
|
|
221
|
+
try {
|
|
222
|
+
const url = new URL(value);
|
|
223
|
+
return {
|
|
224
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
225
|
+
state: url.searchParams.get("state") ?? void 0
|
|
226
|
+
};
|
|
227
|
+
} catch {}
|
|
228
|
+
if (value.includes("#")) {
|
|
229
|
+
const [code, state] = value.split("#", 2);
|
|
230
|
+
return {
|
|
231
|
+
code,
|
|
232
|
+
state
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (value.includes("code=")) {
|
|
236
|
+
const params = new URLSearchParams(value);
|
|
237
|
+
return {
|
|
238
|
+
code: params.get("code") ?? void 0,
|
|
239
|
+
state: params.get("state") ?? void 0
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { code: value };
|
|
243
|
+
}
|
|
244
|
+
function decodeJwt(token) {
|
|
245
|
+
try {
|
|
246
|
+
const parts = token.split(".");
|
|
247
|
+
if (parts.length !== 3) return null;
|
|
248
|
+
const payload = parts[1] ?? "";
|
|
249
|
+
const decoded = atob(payload);
|
|
250
|
+
return JSON.parse(decoded);
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function fetchWithLoginCancellation(input, init) {
|
|
256
|
+
try {
|
|
257
|
+
return await fetch(input, init);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (init.signal?.aborted) throw new Error("Login cancelled");
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function readTokenResponse(response, operation) {
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const text = await response.text().catch(() => "");
|
|
266
|
+
throw new Error(`OpenAI Codex token ${operation} failed (${response.status}): ${text || response.statusText}`);
|
|
267
|
+
}
|
|
268
|
+
const json = await response.json();
|
|
269
|
+
if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") throw new Error(`OpenAI Codex token ${operation} response missing fields: ${JSON.stringify(json)}`);
|
|
270
|
+
return {
|
|
271
|
+
access: json.access_token,
|
|
272
|
+
refresh: json.refresh_token,
|
|
273
|
+
expires: Date.now() + json.expires_in * 1e3
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function exchangeAuthorizationCode(code, verifier, redirectUri, signal) {
|
|
277
|
+
return readTokenResponse(await fetchWithLoginCancellation(TOKEN_URL, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
280
|
+
body: new URLSearchParams({
|
|
281
|
+
grant_type: "authorization_code",
|
|
282
|
+
client_id: CLIENT_ID,
|
|
283
|
+
code,
|
|
284
|
+
code_verifier: verifier,
|
|
285
|
+
redirect_uri: redirectUri
|
|
286
|
+
}),
|
|
287
|
+
signal
|
|
288
|
+
}), "exchange");
|
|
289
|
+
}
|
|
290
|
+
async function refreshAccessToken(refreshToken, signal) {
|
|
291
|
+
let response;
|
|
292
|
+
try {
|
|
293
|
+
response = await fetch(TOKEN_URL, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
296
|
+
body: new URLSearchParams({
|
|
297
|
+
grant_type: "refresh_token",
|
|
298
|
+
refresh_token: refreshToken,
|
|
299
|
+
client_id: CLIENT_ID
|
|
300
|
+
}),
|
|
301
|
+
signal
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new Error(`OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
return readTokenResponse(response, "refresh");
|
|
307
|
+
}
|
|
308
|
+
async function startOpenAICodexDeviceAuth(signal) {
|
|
309
|
+
const response = await fetchWithLoginCancellation(DEVICE_USER_CODE_URL, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "Content-Type": "application/json" },
|
|
312
|
+
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
313
|
+
signal
|
|
314
|
+
});
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
if (response.status === 404) throw new Error("OpenAI Codex device code login is not enabled for this server. Use browser login or verify the server URL.");
|
|
317
|
+
const responseBody = await response.text().catch(() => "");
|
|
318
|
+
throw new Error(`OpenAI Codex device code request failed with status ${response.status}${responseBody ? `: ${responseBody}` : ""}`);
|
|
319
|
+
}
|
|
320
|
+
const json = await response.json();
|
|
321
|
+
const intervalSeconds = typeof json?.interval === "string" ? Number(json.interval.trim()) : json?.interval;
|
|
322
|
+
if (!json?.device_auth_id || !json.user_code || typeof intervalSeconds !== "number" || !Number.isFinite(intervalSeconds) || intervalSeconds < 0) throw new Error(`Invalid OpenAI Codex device code response: ${JSON.stringify(json)}`);
|
|
323
|
+
return {
|
|
324
|
+
deviceAuthId: json.device_auth_id,
|
|
325
|
+
userCode: json.user_code,
|
|
326
|
+
intervalSeconds
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function pollOpenAICodexDeviceAuth(device, signal) {
|
|
330
|
+
return pollOAuthDeviceCodeFlow({
|
|
331
|
+
intervalSeconds: device.intervalSeconds,
|
|
332
|
+
expiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,
|
|
333
|
+
signal,
|
|
334
|
+
poll: async () => {
|
|
335
|
+
const response = await fetchWithLoginCancellation(DEVICE_TOKEN_URL, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "Content-Type": "application/json" },
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
device_auth_id: device.deviceAuthId,
|
|
340
|
+
user_code: device.userCode
|
|
341
|
+
}),
|
|
342
|
+
signal
|
|
343
|
+
});
|
|
344
|
+
if (response.ok) {
|
|
345
|
+
const json = await response.json();
|
|
346
|
+
if (!json?.authorization_code || !json.code_verifier) return {
|
|
347
|
+
status: "failed",
|
|
348
|
+
message: `Invalid OpenAI Codex device auth token response: ${JSON.stringify(json)}`
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
status: "complete",
|
|
352
|
+
value: {
|
|
353
|
+
authorizationCode: json.authorization_code,
|
|
354
|
+
codeVerifier: json.code_verifier
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (response.status === 403 || response.status === 404) return { status: "pending" };
|
|
359
|
+
const responseBody = await response.text().catch(() => "");
|
|
360
|
+
let errorCode;
|
|
361
|
+
try {
|
|
362
|
+
const error = JSON.parse(responseBody)?.error;
|
|
363
|
+
errorCode = typeof error === "object" ? error?.code : error;
|
|
364
|
+
} catch {}
|
|
365
|
+
if (errorCode === "deviceauth_authorization_pending") return { status: "pending" };
|
|
366
|
+
if (errorCode === "slow_down") return { status: "slow_down" };
|
|
367
|
+
return {
|
|
368
|
+
status: "failed",
|
|
369
|
+
message: `OpenAI Codex device auth failed with status ${response.status}${responseBody ? `: ${responseBody}` : ""}`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async function createAuthorizationFlow(originator = "pi") {
|
|
375
|
+
const { verifier, challenge } = await generatePKCE();
|
|
376
|
+
const state = createState();
|
|
377
|
+
const url = new URL(AUTHORIZE_URL);
|
|
378
|
+
url.searchParams.set("response_type", "code");
|
|
379
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
380
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
381
|
+
url.searchParams.set("scope", SCOPE);
|
|
382
|
+
url.searchParams.set("code_challenge", challenge);
|
|
383
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
384
|
+
url.searchParams.set("state", state);
|
|
385
|
+
url.searchParams.set("id_token_add_organizations", "true");
|
|
386
|
+
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
387
|
+
url.searchParams.set("originator", originator);
|
|
388
|
+
return {
|
|
389
|
+
verifier,
|
|
390
|
+
state,
|
|
391
|
+
url: url.toString()
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function startLocalOAuthServer(state) {
|
|
395
|
+
if (!_http) throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
|
|
396
|
+
let settleWait;
|
|
397
|
+
const waitForCodePromise = new Promise((resolve) => {
|
|
398
|
+
let settled = false;
|
|
399
|
+
settleWait = (value) => {
|
|
400
|
+
if (settled) return;
|
|
401
|
+
settled = true;
|
|
402
|
+
resolve(value);
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
const server = _http.createServer((req, res) => {
|
|
406
|
+
try {
|
|
407
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
408
|
+
if (url.pathname !== "/auth/callback") {
|
|
409
|
+
res.statusCode = 404;
|
|
410
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
411
|
+
res.end(oauthErrorHtml("Callback route not found."));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (url.searchParams.get("state") !== state) {
|
|
415
|
+
res.statusCode = 400;
|
|
416
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
417
|
+
res.end(oauthErrorHtml("State mismatch."));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const code = url.searchParams.get("code");
|
|
421
|
+
if (!code) {
|
|
422
|
+
res.statusCode = 400;
|
|
423
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
424
|
+
res.end(oauthErrorHtml("Missing authorization code."));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
res.statusCode = 200;
|
|
428
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
429
|
+
res.end(oauthSuccessHtml("OpenAI authentication completed. You can close this window."));
|
|
430
|
+
settleWait?.({ code });
|
|
431
|
+
} catch {
|
|
432
|
+
res.statusCode = 500;
|
|
433
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
434
|
+
res.end(oauthErrorHtml("Internal error while processing OAuth callback."));
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
server.listen(1455, getCallbackHost(), () => {
|
|
439
|
+
resolve({
|
|
440
|
+
close: () => server.close(),
|
|
441
|
+
cancelWait: () => {
|
|
442
|
+
settleWait?.(null);
|
|
443
|
+
},
|
|
444
|
+
waitForCode: () => waitForCodePromise
|
|
445
|
+
});
|
|
446
|
+
}).on("error", (_err) => {
|
|
447
|
+
settleWait?.(null);
|
|
448
|
+
resolve({
|
|
449
|
+
close: () => {
|
|
450
|
+
try {
|
|
451
|
+
server.close();
|
|
452
|
+
} catch {}
|
|
453
|
+
},
|
|
454
|
+
cancelWait: () => {},
|
|
455
|
+
waitForCode: async () => null
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function getAccountId(accessToken) {
|
|
461
|
+
const accountId = (decodeJwt(accessToken)?.[JWT_CLAIM_PATH])?.chatgpt_account_id;
|
|
462
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
463
|
+
}
|
|
464
|
+
function credentialsFromToken(token) {
|
|
465
|
+
const accountId = getAccountId(token.access);
|
|
466
|
+
if (!accountId) throw new Error("Failed to extract accountId from token");
|
|
467
|
+
return {
|
|
468
|
+
type: "oauth",
|
|
469
|
+
access: token.access,
|
|
470
|
+
refresh: token.refresh,
|
|
471
|
+
expires: token.expires,
|
|
472
|
+
accountId
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function exchangeAuthorizationCodeForCredentials(code, verifier, redirectUri, signal) {
|
|
476
|
+
return credentialsFromToken(await exchangeAuthorizationCode(code, verifier, redirectUri, signal));
|
|
477
|
+
}
|
|
478
|
+
async function loginOpenAICodexDeviceCode(interaction) {
|
|
479
|
+
const device = await startOpenAICodexDeviceAuth(interaction.signal);
|
|
480
|
+
interaction.notify({
|
|
481
|
+
type: "device_code",
|
|
482
|
+
userCode: device.userCode,
|
|
483
|
+
verificationUri: DEVICE_VERIFICATION_URI,
|
|
484
|
+
intervalSeconds: device.intervalSeconds,
|
|
485
|
+
expiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS
|
|
486
|
+
});
|
|
487
|
+
const code = await pollOpenAICodexDeviceAuth(device, interaction.signal);
|
|
488
|
+
return exchangeAuthorizationCodeForCredentials(code.authorizationCode, code.codeVerifier, DEVICE_REDIRECT_URI, interaction.signal);
|
|
489
|
+
}
|
|
490
|
+
async function loginOpenAICodex(interaction) {
|
|
491
|
+
const { verifier, state, url } = await createAuthorizationFlow();
|
|
492
|
+
const server = await startLocalOAuthServer(state);
|
|
493
|
+
const manualAbort = new AbortController();
|
|
494
|
+
const onAbort = () => server.cancelWait();
|
|
495
|
+
interaction.signal.addEventListener("abort", onAbort, { once: true });
|
|
496
|
+
if (interaction.signal.aborted) onAbort();
|
|
497
|
+
let code;
|
|
498
|
+
let manualCode;
|
|
499
|
+
let manualError;
|
|
500
|
+
interaction.notify({
|
|
501
|
+
type: "auth_url",
|
|
502
|
+
url,
|
|
503
|
+
instructions: "A browser window should open. Complete login to finish."
|
|
504
|
+
});
|
|
505
|
+
try {
|
|
506
|
+
const manualPromise = interaction.prompt({
|
|
507
|
+
type: "manual_code",
|
|
508
|
+
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
|
509
|
+
placeholder: REDIRECT_URI,
|
|
510
|
+
signal: manualAbort.signal
|
|
511
|
+
}).then((input) => {
|
|
512
|
+
manualCode = input;
|
|
513
|
+
server.cancelWait();
|
|
514
|
+
}).catch((error) => {
|
|
515
|
+
manualError = error instanceof Error ? error : new Error(String(error));
|
|
516
|
+
server.cancelWait();
|
|
517
|
+
});
|
|
518
|
+
const result = await server.waitForCode();
|
|
519
|
+
if (manualError) throw manualError;
|
|
520
|
+
if (result?.code) code = result.code;
|
|
521
|
+
else if (manualCode) {
|
|
522
|
+
const parsed = parseAuthorizationInput(manualCode);
|
|
523
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
524
|
+
code = parsed.code;
|
|
525
|
+
}
|
|
526
|
+
if (!code) {
|
|
527
|
+
await manualPromise;
|
|
528
|
+
if (manualError) throw manualError;
|
|
529
|
+
if (manualCode) {
|
|
530
|
+
const parsed = parseAuthorizationInput(manualCode);
|
|
531
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
532
|
+
code = parsed.code;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!code) throw new Error("Missing authorization code");
|
|
536
|
+
return exchangeAuthorizationCodeForCredentials(code, verifier, REDIRECT_URI, interaction.signal);
|
|
537
|
+
} finally {
|
|
538
|
+
interaction.signal.removeEventListener("abort", onAbort);
|
|
539
|
+
manualAbort.abort();
|
|
540
|
+
server.close();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Refresh OpenAI Codex OAuth token
|
|
545
|
+
*/
|
|
546
|
+
async function refreshOpenAICodexToken(refreshToken, signal) {
|
|
547
|
+
return credentialsFromToken(await refreshAccessToken(refreshToken, signal));
|
|
548
|
+
}
|
|
549
|
+
const openaiCodexOAuth = {
|
|
550
|
+
name: "OpenAI (ChatGPT Plus/Pro)",
|
|
551
|
+
isSubscription: true,
|
|
552
|
+
async login(interaction) {
|
|
553
|
+
const method = await interaction.prompt({
|
|
554
|
+
type: "select",
|
|
555
|
+
message: "Select OpenAI Codex login method:",
|
|
556
|
+
options: [{
|
|
557
|
+
id: OPENAI_CODEX_BROWSER_LOGIN_METHOD,
|
|
558
|
+
label: "Browser login (default)"
|
|
559
|
+
}, {
|
|
560
|
+
id: OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD,
|
|
561
|
+
label: "Device code login (headless)"
|
|
562
|
+
}]
|
|
563
|
+
});
|
|
564
|
+
if (method === OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD) return loginOpenAICodexDeviceCode(interaction);
|
|
565
|
+
if (method !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) throw new Error(`Unknown OpenAI Codex login method: ${method}`);
|
|
566
|
+
return loginOpenAICodex(interaction);
|
|
567
|
+
},
|
|
568
|
+
refresh: (credential, signal) => refreshOpenAICodexToken(credential.refresh, signal),
|
|
569
|
+
async toAuth(credential) {
|
|
570
|
+
return { apiKey: credential.access };
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
//#endregion
|
|
574
|
+
export { openaiCodexOAuth };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
4
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region node_modules/@earendil-works/pi-ai/dist/utils/provider-env.js
|
|
7
|
+
let procEnvCache = null;
|
|
8
|
+
/**
|
|
9
|
+
* Fallback for https://github.com/oven-sh/bun/issues/27802.
|
|
10
|
+
* Bun compiled binaries can expose an empty process.env inside Linux sandboxes
|
|
11
|
+
* even though /proc/self/environ contains the environment.
|
|
12
|
+
*
|
|
13
|
+
* This intentionally duplicates restoreSandboxEnv() in
|
|
14
|
+
* packages/coding-agent/src/bun/restore-sandbox-env.ts. The ai package can be
|
|
15
|
+
* used directly, without going through that entrypoint, so provider env lookup
|
|
16
|
+
* must not depend on process.env having been patched.
|
|
17
|
+
*/
|
|
18
|
+
function getBunSandboxEnvValue(name) {
|
|
19
|
+
if (typeof process === "undefined" || !process.versions?.bun || Object.keys(process.env).length > 0) return;
|
|
20
|
+
if (procEnvCache === null) {
|
|
21
|
+
procEnvCache = /* @__PURE__ */ new Map();
|
|
22
|
+
try {
|
|
23
|
+
const { readFileSync } = __require("node:fs");
|
|
24
|
+
const data = readFileSync("/proc/self/environ", "utf-8");
|
|
25
|
+
for (const entry of data.split("\0")) {
|
|
26
|
+
const idx = entry.indexOf("=");
|
|
27
|
+
if (idx > 0) procEnvCache.set(entry.slice(0, idx), entry.slice(idx + 1));
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
return procEnvCache.get(name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a provider env value from scoped overrides, normal process.env, then
|
|
35
|
+
* the duplicated Bun sandbox fallback for direct pi-ai consumers.
|
|
36
|
+
*/
|
|
37
|
+
function getProviderEnvValue(name, env) {
|
|
38
|
+
return env?.[name] || (typeof process !== "undefined" ? process.env[name] : void 0) || getBunSandboxEnvValue(name) || void 0;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { __commonJSMin as n, getProviderEnvValue as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-subscription-oauth-plugin",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Out-of-tree DSH bundle for ChatGPT Codex OAuth and live models",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"types": "lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"import": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"README.md",
|
|
22
|
+
"README.ja.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"THIRD_PARTY_NOTICES.md",
|
|
25
|
+
"package.json"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
29
|
+
},
|
|
30
|
+
"packageManager": "pnpm@11.7.0",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"pack:check": "npm pack --dry-run"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-authorization": "0.1.2-alpha.3"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
47
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
48
|
+
"@deepseek-ai/dsh-base": "0.1.2-alpha.3",
|
|
49
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
50
|
+
"@deepseek-ai/dsh-credentials": "0.1.2-alpha.3",
|
|
51
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
53
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.2-alpha.3",
|
|
54
|
+
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.3"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
58
|
+
"@deepseek-ai/cordis-plugin-include": "1.0.7",
|
|
59
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.3",
|
|
60
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
61
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.3",
|
|
62
|
+
"@deepseek-ai/dsh-attachment": "0.1.2-alpha.3",
|
|
63
|
+
"@deepseek-ai/dsh-authorization": "0.1.2-alpha.3",
|
|
64
|
+
"@deepseek-ai/dsh-base": "0.1.2-alpha.3",
|
|
65
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
66
|
+
"@deepseek-ai/dsh-credentials": "0.1.2-alpha.3",
|
|
67
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.2-alpha.3",
|
|
68
|
+
"@deepseek-ai/dsh-fs": "0.1.2-alpha.3",
|
|
69
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-alpha.3",
|
|
70
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
71
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.2-alpha.3",
|
|
72
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
73
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.2-alpha.3",
|
|
74
|
+
"@deepseek-ai/dsh-scope": "0.1.2-alpha.3",
|
|
75
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
76
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
|
|
77
|
+
"@deepseek-ai/dsh-timeout": "0.1.2-alpha.3",
|
|
78
|
+
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.3",
|
|
79
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
80
|
+
"@types/js-yaml": "4.0.9",
|
|
81
|
+
"@types/node": "22.20.0",
|
|
82
|
+
"js-yaml": "4.3.2",
|
|
83
|
+
"tsdown": "0.22.2",
|
|
84
|
+
"typescript": "6.0.3",
|
|
85
|
+
"vitest": "4.1.11"
|
|
86
|
+
}
|
|
87
|
+
}
|