@modelprofile.com/flexharness-providers 7.0.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/dist_ts_media_openai/image.d.ts +46 -0
- package/dist_ts_media_openai/image.js +110 -0
- package/dist_ts_media_openai/index.d.ts +10 -0
- package/dist_ts_media_openai/index.js +16 -0
- package/dist_ts_media_openai/plugins.d.ts +3 -0
- package/dist_ts_media_openai/plugins.js +4 -0
- package/dist_ts_openai_account/index.d.ts +6 -0
- package/dist_ts_openai_account/index.js +7 -0
- package/dist_ts_openai_account/plugins.d.ts +7 -0
- package/dist_ts_openai_account/plugins.js +6 -0
- package/dist_ts_openai_account/smartai.providers.credentials.d.ts +7 -0
- package/dist_ts_openai_account/smartai.providers.credentials.js +126 -0
- package/dist_ts_openai_account/smartai.providers.error.d.ts +11 -0
- package/dist_ts_openai_account/smartai.providers.error.js +30 -0
- package/dist_ts_openai_account/smartai.providers.interfaces.d.ts +161 -0
- package/dist_ts_openai_account/smartai.providers.interfaces.js +2 -0
- package/dist_ts_openai_account/smartai.providers.openai.d.ts +29 -0
- package/dist_ts_openai_account/smartai.providers.openai.js +1075 -0
- package/dist_ts_openai_account/smartai.providers.registry.d.ts +11 -0
- package/dist_ts_openai_account/smartai.providers.registry.js +53 -0
- package/dist_ts_openai_auth/index.d.ts +6 -0
- package/dist_ts_openai_auth/index.js +10 -0
- package/dist_ts_openai_auth/interfaces.d.ts +71 -0
- package/dist_ts_openai_auth/interfaces.js +2 -0
- package/dist_ts_openai_auth/smartai.auth.openai.d.ts +34 -0
- package/dist_ts_openai_auth/smartai.auth.openai.js +603 -0
- package/dist_ts_openai_auth/smartai.middleware.openai.d.ts +6 -0
- package/dist_ts_openai_auth/smartai.middleware.openai.js +47 -0
- package/dist_ts_openai_auth_files/index.d.ts +43 -0
- package/dist_ts_openai_auth_files/index.js +377 -0
- package/dist_ts_openai_auth_files/plugins.d.ts +9 -0
- package/dist_ts_openai_auth_files/plugins.js +10 -0
- package/dist_ts_providers/anthropic/index.d.ts +8 -0
- package/dist_ts_providers/anthropic/index.js +13 -0
- package/dist_ts_providers/anthropic/plugins.d.ts +4 -0
- package/dist_ts_providers/anthropic/plugins.js +4 -0
- package/dist_ts_providers/anthropic/smartai.middleware.anthropic.d.ts +8 -0
- package/dist_ts_providers/anthropic/smartai.middleware.anthropic.js +10 -0
- package/dist_ts_providers/google/index.d.ts +5 -0
- package/dist_ts_providers/google/index.js +6 -0
- package/dist_ts_providers/google/plugins.d.ts +3 -0
- package/dist_ts_providers/google/plugins.js +3 -0
- package/dist_ts_providers/groq/index.d.ts +5 -0
- package/dist_ts_providers/groq/index.js +6 -0
- package/dist_ts_providers/groq/plugins.d.ts +3 -0
- package/dist_ts_providers/groq/plugins.js +3 -0
- package/dist_ts_providers/index.d.ts +8 -0
- package/dist_ts_providers/index.js +9 -0
- package/dist_ts_providers/mistral/index.d.ts +5 -0
- package/dist_ts_providers/mistral/index.js +6 -0
- package/dist_ts_providers/mistral/plugins.d.ts +3 -0
- package/dist_ts_providers/mistral/plugins.js +3 -0
- package/dist_ts_providers/ollama/index.d.ts +6 -0
- package/dist_ts_providers/ollama/index.js +7 -0
- package/dist_ts_providers/ollama/interfaces.d.ts +26 -0
- package/dist_ts_providers/ollama/interfaces.js +2 -0
- package/dist_ts_providers/ollama/smartai.provider.ollama.d.ts +8 -0
- package/dist_ts_providers/ollama/smartai.provider.ollama.js +378 -0
- package/dist_ts_providers/openai/index.d.ts +15 -0
- package/dist_ts_providers/openai/index.js +14 -0
- package/dist_ts_providers/openai/plugins.d.ts +6 -0
- package/dist_ts_providers/openai/plugins.js +4 -0
- package/dist_ts_providers/perplexity/index.d.ts +5 -0
- package/dist_ts_providers/perplexity/index.js +6 -0
- package/dist_ts_providers/perplexity/plugins.d.ts +3 -0
- package/dist_ts_providers/perplexity/plugins.js +3 -0
- package/dist_ts_providers/xai/index.d.ts +5 -0
- package/dist_ts_providers/xai/index.js +6 -0
- package/dist_ts_providers/xai/plugins.d.ts +3 -0
- package/dist_ts_providers/xai/plugins.js +3 -0
- package/dist_ts_research/index.d.ts +19 -0
- package/dist_ts_research/index.js +98 -0
- package/dist_ts_research/plugins.d.ts +2 -0
- package/dist_ts_research/plugins.js +3 -0
- package/license.md +21 -0
- package/openai-codex-license.txt +201 -0
- package/package.json +105 -0
- package/readme.md +62 -0
- package/third-party-notices.md +19 -0
- package/ts_media_openai/image.ts +147 -0
- package/ts_media_openai/index.ts +26 -0
- package/ts_media_openai/plugins.ts +3 -0
- package/ts_media_openai/readme.md +37 -0
- package/ts_openai_account/index.ts +11 -0
- package/ts_openai_account/plugins.ts +9 -0
- package/ts_openai_account/readme.md +37 -0
- package/ts_openai_account/smartai.providers.credentials.ts +163 -0
- package/ts_openai_account/smartai.providers.error.ts +49 -0
- package/ts_openai_account/smartai.providers.interfaces.ts +222 -0
- package/ts_openai_account/smartai.providers.openai.ts +1209 -0
- package/ts_openai_account/smartai.providers.registry.ts +57 -0
- package/ts_openai_auth/index.ts +12 -0
- package/ts_openai_auth/interfaces.ts +89 -0
- package/ts_openai_auth/readme.md +37 -0
- package/ts_openai_auth/smartai.auth.openai.ts +789 -0
- package/ts_openai_auth/smartai.middleware.openai.ts +55 -0
- package/ts_openai_auth_files/index.ts +499 -0
- package/ts_openai_auth_files/plugins.ts +13 -0
- package/ts_openai_auth_files/readme.md +37 -0
- package/ts_providers/anthropic/index.ts +20 -0
- package/ts_providers/anthropic/plugins.ts +4 -0
- package/ts_providers/anthropic/readme.md +37 -0
- package/ts_providers/anthropic/smartai.middleware.anthropic.ts +12 -0
- package/ts_providers/google/index.ts +8 -0
- package/ts_providers/google/plugins.ts +3 -0
- package/ts_providers/google/readme.md +37 -0
- package/ts_providers/groq/index.ts +8 -0
- package/ts_providers/groq/plugins.ts +3 -0
- package/ts_providers/groq/readme.md +37 -0
- package/ts_providers/index.ts +8 -0
- package/ts_providers/mistral/index.ts +8 -0
- package/ts_providers/mistral/plugins.ts +3 -0
- package/ts_providers/mistral/readme.md +37 -0
- package/ts_providers/ollama/index.ts +8 -0
- package/ts_providers/ollama/interfaces.ts +28 -0
- package/ts_providers/ollama/readme.md +37 -0
- package/ts_providers/ollama/smartai.provider.ollama.ts +426 -0
- package/ts_providers/openai/index.ts +29 -0
- package/ts_providers/openai/plugins.ts +6 -0
- package/ts_providers/openai/readme.md +37 -0
- package/ts_providers/perplexity/index.ts +8 -0
- package/ts_providers/perplexity/plugins.ts +3 -0
- package/ts_providers/perplexity/readme.md +37 -0
- package/ts_providers/readme.md +62 -0
- package/ts_providers/xai/index.ts +8 -0
- package/ts_providers/xai/plugins.ts +3 -0
- package/ts_providers/xai/readme.md +37 -0
- package/ts_research/index.ts +120 -0
- package/ts_research/plugins.ts +2 -0
- package/ts_research/readme.md +37 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
// OpenAI protocol portions are adapted in TypeScript; see third-party-notices.md.
|
|
2
|
+
import type {
|
|
3
|
+
IOpenAiChatGptAuthCredentials,
|
|
4
|
+
IOpenAiChatGptAuthOptions,
|
|
5
|
+
IOpenAiChatGptBrowserAuthorization,
|
|
6
|
+
IOpenAiChatGptBrowserAuthorizationOptions,
|
|
7
|
+
IOpenAiChatGptBrowserCallbackOptions,
|
|
8
|
+
IOpenAiChatGptBrowserExchangeOptions,
|
|
9
|
+
IOpenAiChatGptCompleteDeviceCodeOptions,
|
|
10
|
+
IOpenAiChatGptDeviceCode,
|
|
11
|
+
IOpenAiChatGptDeviceCodePollOptions,
|
|
12
|
+
IOpenAiChatGptRevokeOptions,
|
|
13
|
+
IOpenAiChatGptTokenData,
|
|
14
|
+
IOpenAiChatGptTokenInfo,
|
|
15
|
+
TOpenAiChatGptBrowserCallback,
|
|
16
|
+
} from './interfaces.js';
|
|
17
|
+
|
|
18
|
+
export const OPENAI_CHATGPT_AUTH_ISSUER = 'https://auth.openai.com';
|
|
19
|
+
export const OPENAI_CHATGPT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
20
|
+
export const OPENAI_CHATGPT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
21
|
+
export const OPENAI_CHATGPT_DEFAULT_ORIGINATOR = 'smartai';
|
|
22
|
+
|
|
23
|
+
const DEVICE_CODE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
24
|
+
const SAFE_MAX_RESPONSE_BYTES = 64 * 1024;
|
|
25
|
+
const SAFE_MAX_TOKEN_BYTES = 16 * 1024;
|
|
26
|
+
const SAFE_MAX_AUTHORIZATION_CODE_BYTES = 8 * 1024;
|
|
27
|
+
const SAFE_MAX_IDENTIFIER_BYTES = 1024;
|
|
28
|
+
const SAFE_MAX_URL_BYTES = 4096;
|
|
29
|
+
const AUTH_REQUEST_TIMEOUT_MS = 30_000;
|
|
30
|
+
const browserRedirectUris = new Set([
|
|
31
|
+
'http://localhost:1455/auth/callback',
|
|
32
|
+
'http://localhost:1457/auth/callback',
|
|
33
|
+
]);
|
|
34
|
+
const browserScope = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
35
|
+
const base64UrlPattern = /^[A-Za-z0-9_-]+$/;
|
|
36
|
+
const textEncoder = new TextEncoder();
|
|
37
|
+
const textDecoder = new TextDecoder();
|
|
38
|
+
|
|
39
|
+
const usesSafeErrors = (optionsArg: IOpenAiChatGptAuthOptions): boolean =>
|
|
40
|
+
optionsArg.safeErrors !== false;
|
|
41
|
+
const utf8ByteLength = (valueArg: string): number => textEncoder.encode(valueArg).byteLength;
|
|
42
|
+
|
|
43
|
+
export class OpenAiChatGptAuthError extends Error {
|
|
44
|
+
public status?: number;
|
|
45
|
+
public body?: string;
|
|
46
|
+
|
|
47
|
+
constructor(message: string, options: { status?: number; body?: string } = {}) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'OpenAiChatGptAuthError';
|
|
50
|
+
this.status = options.status;
|
|
51
|
+
this.body = options.body;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IOpenAiChatGptAuthorizationCode {
|
|
56
|
+
authorizationCode: string;
|
|
57
|
+
codeChallenge: string;
|
|
58
|
+
codeVerifier: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface IOpenAiChatGptTokenResponse {
|
|
62
|
+
id_token?: unknown;
|
|
63
|
+
access_token?: unknown;
|
|
64
|
+
refresh_token?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getFetch(options: IOpenAiChatGptAuthOptions): typeof fetch {
|
|
68
|
+
const fetchFunction = options.fetch ?? globalThis.fetch;
|
|
69
|
+
if (!fetchFunction) {
|
|
70
|
+
throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT authentication.');
|
|
71
|
+
}
|
|
72
|
+
return fetchFunction;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function authError(
|
|
76
|
+
message: string,
|
|
77
|
+
options: IOpenAiChatGptAuthOptions,
|
|
78
|
+
details: { status?: number; body?: string } = {},
|
|
79
|
+
): OpenAiChatGptAuthError {
|
|
80
|
+
return new OpenAiChatGptAuthError(
|
|
81
|
+
message,
|
|
82
|
+
usesSafeErrors(options)
|
|
83
|
+
? { ...(details.status === undefined ? {} : { status: details.status }) }
|
|
84
|
+
: details,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function throwIfAborted(options: IOpenAiChatGptAuthOptions): void {
|
|
89
|
+
if (options.signal?.aborted) {
|
|
90
|
+
throw authError('OpenAI ChatGPT authentication was aborted.', options);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getIssuer(): string {
|
|
95
|
+
return OPENAI_CHATGPT_AUTH_ISSUER;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getClientId(): string {
|
|
99
|
+
return OPENAI_CHATGPT_CLIENT_ID;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function asString(
|
|
103
|
+
value: unknown,
|
|
104
|
+
name: string,
|
|
105
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
106
|
+
maximumBytes = SAFE_MAX_IDENTIFIER_BYTES,
|
|
107
|
+
): string {
|
|
108
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
109
|
+
throw authError(`OpenAI ChatGPT auth response is missing ${name}.`, options);
|
|
110
|
+
}
|
|
111
|
+
if (usesSafeErrors(options) && utf8ByteLength(value) > maximumBytes) {
|
|
112
|
+
throw authError(`OpenAI ChatGPT auth response contains an invalid ${name}.`, options);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function asOptionalString(
|
|
118
|
+
value: unknown,
|
|
119
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
120
|
+
maximumBytes = SAFE_MAX_IDENTIFIER_BYTES,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
123
|
+
if (usesSafeErrors(options) && utf8ByteLength(value) > maximumBytes) {
|
|
124
|
+
throw authError('OpenAI ChatGPT auth response contains an oversized string.', options);
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function asIntervalSeconds(value: unknown, options: IOpenAiChatGptAuthOptions = {}): number {
|
|
130
|
+
const interval = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
|
|
131
|
+
if (!Number.isFinite(interval) || interval <= 0 || (usesSafeErrors(options) && interval > 300)) {
|
|
132
|
+
throw authError('OpenAI ChatGPT device-code response has an invalid interval.', options);
|
|
133
|
+
}
|
|
134
|
+
return interval;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readResponseText(
|
|
138
|
+
response: Response,
|
|
139
|
+
options: IOpenAiChatGptAuthOptions,
|
|
140
|
+
): Promise<string> {
|
|
141
|
+
if (!usesSafeErrors(options)) return response.text();
|
|
142
|
+
const contentLength = response.headers.get('content-length');
|
|
143
|
+
if (
|
|
144
|
+
contentLength !== null
|
|
145
|
+
&& (!/^\d+$/.test(contentLength) || Number(contentLength) > SAFE_MAX_RESPONSE_BYTES)
|
|
146
|
+
) {
|
|
147
|
+
await response.body?.cancel().catch(() => undefined);
|
|
148
|
+
throw authError('OpenAI ChatGPT auth response exceeded its size limit.', options, {
|
|
149
|
+
status: response.status,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!response.body) return '';
|
|
153
|
+
const reader = response.body.getReader();
|
|
154
|
+
const chunks: Uint8Array[] = [];
|
|
155
|
+
let byteLength = 0;
|
|
156
|
+
let completed = false;
|
|
157
|
+
const onAbort = (): void => {
|
|
158
|
+
void reader.cancel().catch(() => undefined);
|
|
159
|
+
};
|
|
160
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
161
|
+
if (options.signal?.aborted) onAbort();
|
|
162
|
+
try {
|
|
163
|
+
while (true) {
|
|
164
|
+
throwIfAborted(options);
|
|
165
|
+
const { done, value } = await reader.read();
|
|
166
|
+
throwIfAborted(options);
|
|
167
|
+
if (done) break;
|
|
168
|
+
byteLength += value.byteLength;
|
|
169
|
+
if (byteLength > SAFE_MAX_RESPONSE_BYTES) {
|
|
170
|
+
await reader.cancel().catch(() => undefined);
|
|
171
|
+
throw authError('OpenAI ChatGPT auth response exceeded its size limit.', options, {
|
|
172
|
+
status: response.status,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
chunks.push(value);
|
|
176
|
+
}
|
|
177
|
+
completed = true;
|
|
178
|
+
} finally {
|
|
179
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
180
|
+
if (!completed) void reader.cancel().catch(() => undefined);
|
|
181
|
+
reader.releaseLock();
|
|
182
|
+
}
|
|
183
|
+
const bytes = new Uint8Array(byteLength);
|
|
184
|
+
let offset = 0;
|
|
185
|
+
for (const chunk of chunks) {
|
|
186
|
+
bytes.set(chunk, offset);
|
|
187
|
+
offset += chunk.byteLength;
|
|
188
|
+
}
|
|
189
|
+
return new TextDecoder().decode(bytes);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function readJson(
|
|
193
|
+
response: Response,
|
|
194
|
+
context: string,
|
|
195
|
+
options: IOpenAiChatGptAuthOptions,
|
|
196
|
+
): Promise<unknown> {
|
|
197
|
+
const body = await readResponseText(response, options);
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
throw authError(`${context} failed with status ${response.status}.`, options, {
|
|
200
|
+
status: response.status,
|
|
201
|
+
body,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
return body ? JSON.parse(body) : {};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw authError(
|
|
209
|
+
usesSafeErrors(options)
|
|
210
|
+
? `${context} returned invalid JSON.`
|
|
211
|
+
: `${context} returned invalid JSON: ${(error as Error).message}`,
|
|
212
|
+
options,
|
|
213
|
+
{ status: response.status, body },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runFetch<TResult>(
|
|
219
|
+
url: string,
|
|
220
|
+
init: RequestInit,
|
|
221
|
+
options: IOpenAiChatGptAuthOptions,
|
|
222
|
+
responseHandlerArg: (
|
|
223
|
+
response: Response,
|
|
224
|
+
requestOptions: IOpenAiChatGptAuthOptions,
|
|
225
|
+
) => Promise<TResult>,
|
|
226
|
+
timeoutMsArg = AUTH_REQUEST_TIMEOUT_MS,
|
|
227
|
+
): Promise<TResult> {
|
|
228
|
+
throwIfAborted(options);
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
let response: Response | undefined;
|
|
231
|
+
const onExternalAbort = (): void => controller.abort();
|
|
232
|
+
const onRequestAbort = (): void => {
|
|
233
|
+
void response?.body?.cancel().catch(() => undefined);
|
|
234
|
+
};
|
|
235
|
+
options.signal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
236
|
+
let rejectOnAbort!: () => void;
|
|
237
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
238
|
+
rejectOnAbort = () => reject(new Error('OpenAI ChatGPT authentication request aborted.'));
|
|
239
|
+
controller.signal.addEventListener('abort', rejectOnAbort, { once: true });
|
|
240
|
+
});
|
|
241
|
+
controller.signal.addEventListener('abort', onRequestAbort, { once: true });
|
|
242
|
+
if (options.signal?.aborted) controller.abort();
|
|
243
|
+
const timer = setTimeout(() => controller.abort(), Math.min(timeoutMsArg, AUTH_REQUEST_TIMEOUT_MS));
|
|
244
|
+
const requestOptions = { ...options, signal: controller.signal };
|
|
245
|
+
try {
|
|
246
|
+
return await Promise.race([
|
|
247
|
+
(async () => {
|
|
248
|
+
response = await getFetch(options)(url, {
|
|
249
|
+
...init,
|
|
250
|
+
redirect: 'error',
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
});
|
|
253
|
+
if (controller.signal.aborted) {
|
|
254
|
+
await response.body?.cancel().catch(() => undefined);
|
|
255
|
+
}
|
|
256
|
+
throwIfAborted(requestOptions);
|
|
257
|
+
return responseHandlerArg(response, requestOptions);
|
|
258
|
+
})(),
|
|
259
|
+
aborted,
|
|
260
|
+
]);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof OpenAiChatGptAuthError) throw error;
|
|
263
|
+
if (!usesSafeErrors(options)) throw error;
|
|
264
|
+
throw authError(
|
|
265
|
+
options.signal?.aborted
|
|
266
|
+
? 'OpenAI ChatGPT authentication was aborted.'
|
|
267
|
+
: 'OpenAI ChatGPT authentication request failed.',
|
|
268
|
+
options,
|
|
269
|
+
);
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
options.signal?.removeEventListener('abort', onExternalAbort);
|
|
273
|
+
controller.signal.removeEventListener('abort', rejectOnAbort);
|
|
274
|
+
controller.signal.removeEventListener('abort', onRequestAbort);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function postJson(
|
|
279
|
+
url: string,
|
|
280
|
+
body: unknown,
|
|
281
|
+
options: IOpenAiChatGptAuthOptions,
|
|
282
|
+
): Promise<unknown> {
|
|
283
|
+
return runFetch(url, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
body: JSON.stringify(body),
|
|
287
|
+
}, options, (response, requestOptions) => readJson(response, `POST ${url}`, requestOptions));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function postForm(
|
|
291
|
+
url: string,
|
|
292
|
+
body: URLSearchParams,
|
|
293
|
+
options: IOpenAiChatGptAuthOptions,
|
|
294
|
+
): Promise<unknown> {
|
|
295
|
+
return runFetch(url, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
298
|
+
body: body.toString(),
|
|
299
|
+
}, options, (response, requestOptions) => readJson(response, `POST ${url}`, requestOptions));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function sleep(ms: number): Promise<void> {
|
|
303
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function sleepWithAbort(
|
|
307
|
+
milliseconds: number,
|
|
308
|
+
options: IOpenAiChatGptDeviceCodePollOptions,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
throwIfAborted(options);
|
|
311
|
+
if (!options.signal || options.sleep) {
|
|
312
|
+
if (!options.signal) {
|
|
313
|
+
await (options.sleep ?? sleep)(milliseconds);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
let abortListener: (() => void) | undefined;
|
|
317
|
+
try {
|
|
318
|
+
await Promise.race([
|
|
319
|
+
options.sleep!(milliseconds),
|
|
320
|
+
new Promise<never>((_resolve, reject) => {
|
|
321
|
+
abortListener = () => reject(authError('OpenAI ChatGPT authentication was aborted.', options));
|
|
322
|
+
options.signal!.addEventListener('abort', abortListener, { once: true });
|
|
323
|
+
}),
|
|
324
|
+
]);
|
|
325
|
+
} finally {
|
|
326
|
+
if (abortListener) options.signal.removeEventListener('abort', abortListener);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
await new Promise<void>((resolve, reject) => {
|
|
331
|
+
const onAbort = (): void => {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
options.signal!.removeEventListener('abort', onAbort);
|
|
334
|
+
reject(authError('OpenAI ChatGPT authentication was aborted.', options));
|
|
335
|
+
};
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
options.signal!.removeEventListener('abort', onAbort);
|
|
338
|
+
resolve();
|
|
339
|
+
}, milliseconds);
|
|
340
|
+
options.signal!.addEventListener('abort', onAbort, { once: true });
|
|
341
|
+
if (options.signal!.aborted) onAbort();
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseJwtPayload(jwt: string): Record<string, unknown> {
|
|
346
|
+
const parts = jwt.split('.');
|
|
347
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
348
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth returned an invalid token.');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
353
|
+
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));
|
|
354
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
355
|
+
return JSON.parse(textDecoder.decode(bytes)) as Record<string, unknown>;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT token could not be parsed: ${(error as Error).message}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenInfo {
|
|
362
|
+
const claims = parseJwtPayload(token);
|
|
363
|
+
const profile = claims['https://api.openai.com/profile'] as Record<string, unknown> | undefined;
|
|
364
|
+
const auth = claims['https://api.openai.com/auth'] as Record<string, unknown> | undefined;
|
|
365
|
+
const expiresAtSeconds = typeof claims.exp === 'number' ? claims.exp : undefined;
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
email: asOptionalString(claims.email) ?? asOptionalString(profile?.email),
|
|
369
|
+
chatgptPlanType: asOptionalString(auth?.chatgpt_plan_type),
|
|
370
|
+
chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id),
|
|
371
|
+
chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
|
|
372
|
+
chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
|
|
373
|
+
expiresAt: expiresAtSeconds ? new Date(expiresAtSeconds * 1000).toISOString() : undefined,
|
|
374
|
+
rawJwt: token,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function createTokenData(
|
|
379
|
+
response: IOpenAiChatGptTokenResponse,
|
|
380
|
+
existingTokenData?: IOpenAiChatGptTokenData,
|
|
381
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
382
|
+
): IOpenAiChatGptTokenData {
|
|
383
|
+
const accessToken = asOptionalString(response.access_token, options, SAFE_MAX_TOKEN_BYTES)
|
|
384
|
+
?? existingTokenData?.accessToken;
|
|
385
|
+
const refreshToken = asOptionalString(response.refresh_token, options, SAFE_MAX_TOKEN_BYTES)
|
|
386
|
+
?? existingTokenData?.refreshToken;
|
|
387
|
+
const idToken = asOptionalString(response.id_token, options, SAFE_MAX_TOKEN_BYTES)
|
|
388
|
+
?? existingTokenData?.idToken;
|
|
389
|
+
if (usesSafeErrors(options)) {
|
|
390
|
+
for (const token of [accessToken, refreshToken, idToken]) {
|
|
391
|
+
if (token !== undefined && utf8ByteLength(token) > SAFE_MAX_TOKEN_BYTES) {
|
|
392
|
+
throw authError('OpenAI ChatGPT auth returned an oversized token.', options);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!accessToken) {
|
|
397
|
+
throw authError('OpenAI ChatGPT auth response is missing access_token.', options);
|
|
398
|
+
}
|
|
399
|
+
if (!refreshToken) {
|
|
400
|
+
throw authError('OpenAI ChatGPT auth response is missing refresh_token.', options);
|
|
401
|
+
}
|
|
402
|
+
let tokenInfo: IOpenAiChatGptTokenInfo;
|
|
403
|
+
try {
|
|
404
|
+
tokenInfo = parseOpenAiChatGptTokenInfo(idToken ?? accessToken);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (!usesSafeErrors(options)) throw error;
|
|
407
|
+
throw authError('OpenAI ChatGPT auth returned an invalid token.', options);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
accessToken,
|
|
412
|
+
refreshToken,
|
|
413
|
+
idToken,
|
|
414
|
+
accountId: tokenInfo.chatgptAccountId,
|
|
415
|
+
tokenInfo,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function requestOpenAiChatGptDeviceCode(
|
|
420
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
421
|
+
): Promise<IOpenAiChatGptDeviceCode> {
|
|
422
|
+
const issuer = getIssuer();
|
|
423
|
+
const response = await postJson(`${issuer}/api/accounts/deviceauth/usercode`, {
|
|
424
|
+
client_id: getClientId(),
|
|
425
|
+
}, options) as Record<string, unknown>;
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
verificationUrl: `${issuer}/codex/device`,
|
|
429
|
+
userCode: asString(response.user_code ?? response.usercode, 'user_code', options),
|
|
430
|
+
deviceAuthId: asString(response.device_auth_id, 'device_auth_id', options),
|
|
431
|
+
intervalSeconds: asIntervalSeconds(response.interval, options),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function pollOpenAiChatGptDeviceCode(
|
|
436
|
+
deviceCode: IOpenAiChatGptDeviceCode,
|
|
437
|
+
options: IOpenAiChatGptDeviceCodePollOptions = {},
|
|
438
|
+
): Promise<IOpenAiChatGptAuthorizationCode> {
|
|
439
|
+
const issuer = getIssuer();
|
|
440
|
+
const pollUrl = `${issuer}/api/accounts/deviceauth/token`;
|
|
441
|
+
const timeoutMs = options.timeoutMs ?? DEVICE_CODE_TIMEOUT_MS;
|
|
442
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > DEVICE_CODE_TIMEOUT_MS) {
|
|
443
|
+
throw authError('OpenAI ChatGPT device-code timeout is invalid.', options);
|
|
444
|
+
}
|
|
445
|
+
if (
|
|
446
|
+
usesSafeErrors(options)
|
|
447
|
+
&& (
|
|
448
|
+
utf8ByteLength(deviceCode.deviceAuthId) > SAFE_MAX_IDENTIFIER_BYTES
|
|
449
|
+
|| utf8ByteLength(deviceCode.userCode) > SAFE_MAX_IDENTIFIER_BYTES
|
|
450
|
+
|| !Number.isSafeInteger(deviceCode.intervalSeconds)
|
|
451
|
+
|| deviceCode.intervalSeconds < 1
|
|
452
|
+
|| deviceCode.intervalSeconds > 300
|
|
453
|
+
)
|
|
454
|
+
) {
|
|
455
|
+
throw authError('OpenAI ChatGPT device-code input is invalid.', options);
|
|
456
|
+
}
|
|
457
|
+
const startedAt = Date.now();
|
|
458
|
+
|
|
459
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
460
|
+
const remainingBeforeRequest = timeoutMs - (Date.now() - startedAt);
|
|
461
|
+
const pollResult = await runFetch(pollUrl, {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
device_auth_id: deviceCode.deviceAuthId,
|
|
466
|
+
user_code: deviceCode.userCode,
|
|
467
|
+
}),
|
|
468
|
+
}, options, async (response, requestOptions) => {
|
|
469
|
+
if (response.ok) {
|
|
470
|
+
return {
|
|
471
|
+
status: 'complete' as const,
|
|
472
|
+
body: await readJson(response, `POST ${pollUrl}`, requestOptions) as Record<string, unknown>,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (response.status !== 403 && response.status !== 404) {
|
|
476
|
+
const body = await readResponseText(response, requestOptions);
|
|
477
|
+
throw authError(`OpenAI ChatGPT device-code polling failed with status ${response.status}.`, requestOptions, {
|
|
478
|
+
status: response.status,
|
|
479
|
+
body,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
await readResponseText(response, requestOptions);
|
|
483
|
+
return { status: 'pending' as const };
|
|
484
|
+
}, Math.max(remainingBeforeRequest, 1));
|
|
485
|
+
|
|
486
|
+
if (pollResult.status === 'complete') {
|
|
487
|
+
return {
|
|
488
|
+
authorizationCode: asString(
|
|
489
|
+
pollResult.body.authorization_code,
|
|
490
|
+
'authorization_code',
|
|
491
|
+
options,
|
|
492
|
+
SAFE_MAX_AUTHORIZATION_CODE_BYTES,
|
|
493
|
+
),
|
|
494
|
+
codeChallenge: asString(pollResult.body.code_challenge, 'code_challenge', options),
|
|
495
|
+
codeVerifier: asString(pollResult.body.code_verifier, 'code_verifier', options),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
499
|
+
await sleepWithAbort(
|
|
500
|
+
Math.min(deviceCode.intervalSeconds * 1000, Math.max(remaining, 0)),
|
|
501
|
+
options,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
throw authError('OpenAI ChatGPT device-code login timed out.', options);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function exchangeOpenAiChatGptAuthorizationCode(
|
|
509
|
+
authorizationCode: IOpenAiChatGptAuthorizationCode,
|
|
510
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
511
|
+
): Promise<IOpenAiChatGptTokenData> {
|
|
512
|
+
const issuer = getIssuer();
|
|
513
|
+
const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({
|
|
514
|
+
grant_type: 'authorization_code',
|
|
515
|
+
code: authorizationCode.authorizationCode,
|
|
516
|
+
redirect_uri: `${issuer}/deviceauth/callback`,
|
|
517
|
+
client_id: getClientId(),
|
|
518
|
+
code_verifier: authorizationCode.codeVerifier,
|
|
519
|
+
}), options) as IOpenAiChatGptTokenResponse;
|
|
520
|
+
|
|
521
|
+
return createTokenData(response, undefined, options);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function ensureOpenAiChatGptWorkspaceAllowed(
|
|
525
|
+
tokenData: IOpenAiChatGptTokenData,
|
|
526
|
+
forcedChatGptWorkspaceId?: string,
|
|
527
|
+
): void {
|
|
528
|
+
if (!forcedChatGptWorkspaceId) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (tokenData.tokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) {
|
|
532
|
+
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT login is restricted to workspace ${forcedChatGptWorkspaceId}.`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function completeOpenAiChatGptDeviceCodeLogin(
|
|
537
|
+
deviceCode: IOpenAiChatGptDeviceCode,
|
|
538
|
+
options: IOpenAiChatGptCompleteDeviceCodeOptions = {},
|
|
539
|
+
): Promise<IOpenAiChatGptTokenData> {
|
|
540
|
+
const authorizationCode = await pollOpenAiChatGptDeviceCode(deviceCode, options);
|
|
541
|
+
const tokenData = await exchangeOpenAiChatGptAuthorizationCode(authorizationCode, options);
|
|
542
|
+
ensureOpenAiChatGptWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId);
|
|
543
|
+
return tokenData;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function refreshOpenAiChatGptTokenData(
|
|
547
|
+
tokenData: IOpenAiChatGptTokenData,
|
|
548
|
+
options: IOpenAiChatGptAuthOptions = {},
|
|
549
|
+
): Promise<IOpenAiChatGptTokenData> {
|
|
550
|
+
const issuer = getIssuer();
|
|
551
|
+
const response = await postJson(`${issuer}/oauth/token`, {
|
|
552
|
+
client_id: getClientId(),
|
|
553
|
+
grant_type: 'refresh_token',
|
|
554
|
+
refresh_token: tokenData.refreshToken,
|
|
555
|
+
}, options) as IOpenAiChatGptTokenResponse;
|
|
556
|
+
|
|
557
|
+
return createTokenData({
|
|
558
|
+
id_token: response.id_token ?? tokenData.idToken,
|
|
559
|
+
access_token: response.access_token ?? tokenData.accessToken,
|
|
560
|
+
refresh_token: response.refresh_token ?? tokenData.refreshToken,
|
|
561
|
+
}, tokenData, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const requireBrowserRedirectUri = (
|
|
565
|
+
redirectUri: string,
|
|
566
|
+
options: IOpenAiChatGptAuthOptions,
|
|
567
|
+
): string => {
|
|
568
|
+
if (!browserRedirectUris.has(redirectUri)) {
|
|
569
|
+
throw authError('OpenAI ChatGPT browser redirect URI is not registered.', options);
|
|
570
|
+
}
|
|
571
|
+
return redirectUri;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const requireBase64UrlValue = (
|
|
575
|
+
value: string,
|
|
576
|
+
label: string,
|
|
577
|
+
minimumBytes: number,
|
|
578
|
+
maximumBytes: number,
|
|
579
|
+
): string => {
|
|
580
|
+
if (
|
|
581
|
+
typeof value !== 'string'
|
|
582
|
+
|| !base64UrlPattern.test(value)
|
|
583
|
+
|| value.length < minimumBytes
|
|
584
|
+
|| value.length > maximumBytes
|
|
585
|
+
) {
|
|
586
|
+
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT ${label} is invalid.`);
|
|
587
|
+
}
|
|
588
|
+
return value;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const encodeBase64Url = (bytesArg: Uint8Array): string => {
|
|
592
|
+
let binary = '';
|
|
593
|
+
for (const byte of bytesArg) binary += String.fromCharCode(byte);
|
|
594
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
export async function createOpenAiChatGptBrowserAuthorization(
|
|
598
|
+
options: IOpenAiChatGptBrowserAuthorizationOptions,
|
|
599
|
+
): Promise<IOpenAiChatGptBrowserAuthorization> {
|
|
600
|
+
const issuer = getIssuer();
|
|
601
|
+
const redirectUri = requireBrowserRedirectUri(options.redirectUri, options);
|
|
602
|
+
const webCrypto = globalThis.crypto;
|
|
603
|
+
if (!webCrypto?.subtle) {
|
|
604
|
+
throw authError('Web Crypto is required for OpenAI ChatGPT browser authentication.', options);
|
|
605
|
+
}
|
|
606
|
+
const codeVerifier = encodeBase64Url(webCrypto.getRandomValues(new Uint8Array(64)));
|
|
607
|
+
const state = encodeBase64Url(webCrypto.getRandomValues(new Uint8Array(32)));
|
|
608
|
+
const codeChallenge = encodeBase64Url(new Uint8Array(
|
|
609
|
+
await webCrypto.subtle.digest('SHA-256', textEncoder.encode(codeVerifier)),
|
|
610
|
+
));
|
|
611
|
+
const originator = options.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR;
|
|
612
|
+
if (
|
|
613
|
+
utf8ByteLength(originator) > 128
|
|
614
|
+
|| !/^[A-Za-z0-9._-]+$/.test(originator)
|
|
615
|
+
) {
|
|
616
|
+
throw authError('OpenAI ChatGPT browser originator is invalid.', options);
|
|
617
|
+
}
|
|
618
|
+
const allowedWorkspaceIds = options.allowedWorkspaceIds ?? [];
|
|
619
|
+
if (
|
|
620
|
+
!Array.isArray(allowedWorkspaceIds)
|
|
621
|
+
|| allowedWorkspaceIds.length > 16
|
|
622
|
+
|| allowedWorkspaceIds.some((id) => (
|
|
623
|
+
typeof id !== 'string'
|
|
624
|
+
|| id.length === 0
|
|
625
|
+
|| utf8ByteLength(id) > 256
|
|
626
|
+
|| /[,\s]/.test(id)
|
|
627
|
+
))
|
|
628
|
+
) {
|
|
629
|
+
throw authError('OpenAI ChatGPT allowed workspace IDs are invalid.', options);
|
|
630
|
+
}
|
|
631
|
+
const authUrl = new URL(`${issuer}/oauth/authorize`);
|
|
632
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
633
|
+
authUrl.searchParams.set('client_id', getClientId());
|
|
634
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
635
|
+
authUrl.searchParams.set('scope', browserScope);
|
|
636
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
637
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
638
|
+
authUrl.searchParams.set('id_token_add_organizations', 'true');
|
|
639
|
+
authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
|
|
640
|
+
authUrl.searchParams.set('state', state);
|
|
641
|
+
authUrl.searchParams.set('originator', originator);
|
|
642
|
+
if (allowedWorkspaceIds.length > 0) {
|
|
643
|
+
authUrl.searchParams.set('allowed_workspace_id', allowedWorkspaceIds.join(','));
|
|
644
|
+
}
|
|
645
|
+
if (utf8ByteLength(authUrl.href) > SAFE_MAX_URL_BYTES) {
|
|
646
|
+
throw authError('OpenAI ChatGPT browser authorization URL is too large.', options);
|
|
647
|
+
}
|
|
648
|
+
return { authUrl: authUrl.href, state, codeVerifier, redirectUri };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function parseOpenAiChatGptBrowserCallback(
|
|
652
|
+
callbackUrl: string,
|
|
653
|
+
options: IOpenAiChatGptBrowserCallbackOptions,
|
|
654
|
+
): TOpenAiChatGptBrowserCallback {
|
|
655
|
+
if (
|
|
656
|
+
typeof callbackUrl !== 'string'
|
|
657
|
+
|| utf8ByteLength(callbackUrl) > SAFE_MAX_URL_BYTES
|
|
658
|
+
) {
|
|
659
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback is invalid.');
|
|
660
|
+
}
|
|
661
|
+
const expectedRedirectUri = requireBrowserRedirectUri(options.expectedRedirectUri, {});
|
|
662
|
+
const expectedState = requireBase64UrlValue(options.expectedState, 'browser state', 32, 128);
|
|
663
|
+
let parsed: URL;
|
|
664
|
+
try {
|
|
665
|
+
parsed = new URL(callbackUrl);
|
|
666
|
+
} catch {
|
|
667
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback is invalid.');
|
|
668
|
+
}
|
|
669
|
+
const expectedUrl = new URL(expectedRedirectUri);
|
|
670
|
+
if (parsed.origin !== expectedUrl.origin || parsed.pathname !== expectedUrl.pathname) {
|
|
671
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback target is invalid.');
|
|
672
|
+
}
|
|
673
|
+
const allowedKeys = new Set(['code', 'state', 'error', 'error_description']);
|
|
674
|
+
const keys = [...parsed.searchParams.keys()];
|
|
675
|
+
if (
|
|
676
|
+
parsed.hash.length > 0
|
|
677
|
+
|| keys.some((key) => !allowedKeys.has(key))
|
|
678
|
+
|| new Set(keys).size !== keys.length
|
|
679
|
+
) {
|
|
680
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback parameters are invalid.');
|
|
681
|
+
}
|
|
682
|
+
const state = parsed.searchParams.get('state');
|
|
683
|
+
if (state !== expectedState) {
|
|
684
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback state does not match.');
|
|
685
|
+
}
|
|
686
|
+
const providerError = parsed.searchParams.get('error');
|
|
687
|
+
if (providerError !== null) {
|
|
688
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(providerError)) {
|
|
689
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser login was denied.');
|
|
690
|
+
}
|
|
691
|
+
return { providerError };
|
|
692
|
+
}
|
|
693
|
+
const authorizationCode = parsed.searchParams.get('code');
|
|
694
|
+
if (
|
|
695
|
+
!authorizationCode
|
|
696
|
+
|| utf8ByteLength(authorizationCode) > SAFE_MAX_AUTHORIZATION_CODE_BYTES
|
|
697
|
+
) {
|
|
698
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT browser callback has no valid code.');
|
|
699
|
+
}
|
|
700
|
+
return { authorizationCode };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export async function exchangeOpenAiChatGptBrowserAuthorizationCode(
|
|
704
|
+
authorizationCode: string,
|
|
705
|
+
options: IOpenAiChatGptBrowserExchangeOptions,
|
|
706
|
+
): Promise<IOpenAiChatGptTokenData> {
|
|
707
|
+
const issuer = getIssuer();
|
|
708
|
+
const redirectUri = requireBrowserRedirectUri(options.redirectUri, options);
|
|
709
|
+
if (
|
|
710
|
+
typeof authorizationCode !== 'string'
|
|
711
|
+
|| authorizationCode.length === 0
|
|
712
|
+
|| utf8ByteLength(authorizationCode) > SAFE_MAX_AUTHORIZATION_CODE_BYTES
|
|
713
|
+
) {
|
|
714
|
+
throw authError('OpenAI ChatGPT browser authorization code is invalid.', options);
|
|
715
|
+
}
|
|
716
|
+
const codeVerifier = requireBase64UrlValue(options.codeVerifier, 'PKCE verifier', 43, 128);
|
|
717
|
+
const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({
|
|
718
|
+
grant_type: 'authorization_code',
|
|
719
|
+
code: authorizationCode,
|
|
720
|
+
redirect_uri: redirectUri,
|
|
721
|
+
client_id: getClientId(),
|
|
722
|
+
code_verifier: codeVerifier,
|
|
723
|
+
}), options) as IOpenAiChatGptTokenResponse;
|
|
724
|
+
return createTokenData(response, undefined, options);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export async function revokeOpenAiChatGptToken(
|
|
728
|
+
token: string,
|
|
729
|
+
options: IOpenAiChatGptRevokeOptions,
|
|
730
|
+
): Promise<void> {
|
|
731
|
+
if (
|
|
732
|
+
typeof token !== 'string'
|
|
733
|
+
|| token.length === 0
|
|
734
|
+
|| (usesSafeErrors(options) && utf8ByteLength(token) > SAFE_MAX_TOKEN_BYTES)
|
|
735
|
+
) {
|
|
736
|
+
throw authError('OpenAI ChatGPT revoke token is invalid.', options);
|
|
737
|
+
}
|
|
738
|
+
const issuer = getIssuer();
|
|
739
|
+
await runFetch(`${issuer}/oauth/revoke`, {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: { 'Content-Type': 'application/json' },
|
|
742
|
+
body: JSON.stringify({
|
|
743
|
+
token,
|
|
744
|
+
token_type_hint: options.tokenTypeHint,
|
|
745
|
+
client_id: getClientId(),
|
|
746
|
+
}),
|
|
747
|
+
}, options, async (response, requestOptions) => {
|
|
748
|
+
const body = await readResponseText(response, requestOptions);
|
|
749
|
+
if (!response.ok) {
|
|
750
|
+
throw authError(`POST ${issuer}/oauth/revoke failed with status ${response.status}.`, requestOptions, {
|
|
751
|
+
status: response.status,
|
|
752
|
+
body,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptAuthCredentials): {
|
|
759
|
+
apiKey: string;
|
|
760
|
+
baseURL: string;
|
|
761
|
+
headers: Record<string, string>;
|
|
762
|
+
} {
|
|
763
|
+
const tokenAccountId = credentials.tokenInfo?.chatgptAccountId;
|
|
764
|
+
if (
|
|
765
|
+
credentials.accountId !== undefined
|
|
766
|
+
&& tokenAccountId !== undefined
|
|
767
|
+
&& credentials.accountId !== tokenAccountId
|
|
768
|
+
) {
|
|
769
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT account ID does not match the token claim.');
|
|
770
|
+
}
|
|
771
|
+
const accountId = tokenAccountId ?? credentials.accountId;
|
|
772
|
+
const isFedrampAccount = credentials.tokenInfo?.chatgptAccountIsFedramp === true;
|
|
773
|
+
const headers: Record<string, string> = {
|
|
774
|
+
originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
if (accountId) {
|
|
778
|
+
headers['ChatGPT-Account-ID'] = accountId;
|
|
779
|
+
}
|
|
780
|
+
if (isFedrampAccount) {
|
|
781
|
+
headers['X-OpenAI-Fedramp'] = 'true';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
apiKey: credentials.accessToken,
|
|
786
|
+
baseURL: OPENAI_CHATGPT_CODEX_BASE_URL,
|
|
787
|
+
headers,
|
|
788
|
+
};
|
|
789
|
+
}
|