@modelprofile.com/flexharness-providers 7.0.0 → 8.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_openai_account/smartai.providers.interfaces.d.ts +20 -2
- package/dist_ts_openai_account/smartai.providers.openai.d.ts +3 -3
- package/dist_ts_openai_account/smartai.providers.openai.js +61 -17
- package/dist_ts_openai_auth/interfaces.d.ts +4 -0
- package/dist_ts_openai_auth/smartai.auth.openai.d.ts +5 -1
- package/dist_ts_openai_auth/smartai.auth.openai.js +66 -12
- package/package.json +2 -2
- package/ts_openai_account/readme.md +52 -0
- package/ts_openai_account/smartai.providers.interfaces.ts +25 -2
- package/ts_openai_account/smartai.providers.openai.ts +73 -17
- package/ts_openai_auth/interfaces.ts +5 -1
- package/ts_openai_auth/readme.md +12 -0
- package/ts_openai_auth/smartai.auth.openai.ts +82 -12
package/ts_openai_auth/readme.md
CHANGED
|
@@ -11,6 +11,18 @@ pnpm add @modelprofile.com/flexharness-providers
|
|
|
11
11
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
12
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
13
|
|
|
14
|
+
The refresh helper preserves safe request-outcome metadata on
|
|
15
|
+
`OpenAiChatGptAuthError`: `requestOutcome` is `notSent` only before the HTTP
|
|
16
|
+
request is handed to `fetch`, and `outcomeUnknown` afterward. An observed
|
|
17
|
+
response can also supply `status` and a bounded `retryAfterMs`. A 429 response
|
|
18
|
+
does not prove that a rotating grant remained usable. The default safe-error
|
|
19
|
+
mode omits provider response bodies.
|
|
20
|
+
|
|
21
|
+
`createOpenAiChatGptModelConnection()` derives account and FedRAMP headers from
|
|
22
|
+
the current access token. The token must contain an unexpired `exp` claim;
|
|
23
|
+
supplied `accountId`, `isFedrampAccount`, and `tokenInfo` must agree with its
|
|
24
|
+
claims. A saved ID-token snapshot cannot override current access-token facts.
|
|
25
|
+
|
|
14
26
|
## Issue Reporting and Security
|
|
15
27
|
|
|
16
28
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
IOpenAiChatGptTokenData,
|
|
14
14
|
IOpenAiChatGptTokenInfo,
|
|
15
15
|
TOpenAiChatGptBrowserCallback,
|
|
16
|
+
TOpenAiChatGptRequestOutcome,
|
|
16
17
|
} from './interfaces.js';
|
|
17
18
|
|
|
18
19
|
export const OPENAI_CHATGPT_AUTH_ISSUER = 'https://auth.openai.com';
|
|
@@ -43,12 +44,21 @@ const utf8ByteLength = (valueArg: string): number => textEncoder.encode(valueArg
|
|
|
43
44
|
export class OpenAiChatGptAuthError extends Error {
|
|
44
45
|
public status?: number;
|
|
45
46
|
public body?: string;
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
public requestOutcome?: TOpenAiChatGptRequestOutcome;
|
|
48
|
+
public retryAfterMs?: number;
|
|
49
|
+
|
|
50
|
+
constructor(message: string, options: {
|
|
51
|
+
status?: number;
|
|
52
|
+
body?: string;
|
|
53
|
+
requestOutcome?: TOpenAiChatGptRequestOutcome;
|
|
54
|
+
retryAfterMs?: number;
|
|
55
|
+
} = {}) {
|
|
48
56
|
super(message);
|
|
49
57
|
this.name = 'OpenAiChatGptAuthError';
|
|
50
58
|
this.status = options.status;
|
|
51
59
|
this.body = options.body;
|
|
60
|
+
this.requestOutcome = options.requestOutcome;
|
|
61
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -215,6 +225,17 @@ async function readJson(
|
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
227
|
|
|
228
|
+
const parseRetryAfterMs = (headerArg: string | null): number | undefined => {
|
|
229
|
+
if (!headerArg || headerArg.length > 128) return undefined;
|
|
230
|
+
const maximumDelayMs = 24 * 60 * 60 * 1000;
|
|
231
|
+
if (/^\d{1,10}$/.test(headerArg)) {
|
|
232
|
+
return Math.min(Number(headerArg) * 1000, maximumDelayMs);
|
|
233
|
+
}
|
|
234
|
+
const retryAt = Date.parse(headerArg);
|
|
235
|
+
if (!Number.isFinite(retryAt)) return undefined;
|
|
236
|
+
return Math.min(Math.max(0, retryAt - Date.now()), maximumDelayMs);
|
|
237
|
+
};
|
|
238
|
+
|
|
218
239
|
async function runFetch<TResult>(
|
|
219
240
|
url: string,
|
|
220
241
|
init: RequestInit,
|
|
@@ -225,9 +246,14 @@ async function runFetch<TResult>(
|
|
|
225
246
|
) => Promise<TResult>,
|
|
226
247
|
timeoutMsArg = AUTH_REQUEST_TIMEOUT_MS,
|
|
227
248
|
): Promise<TResult> {
|
|
228
|
-
|
|
249
|
+
if (options.signal?.aborted) {
|
|
250
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT authentication was aborted.', {
|
|
251
|
+
requestOutcome: 'notSent',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
229
254
|
const controller = new AbortController();
|
|
230
255
|
let response: Response | undefined;
|
|
256
|
+
let requestStarted = false;
|
|
231
257
|
const onExternalAbort = (): void => controller.abort();
|
|
232
258
|
const onRequestAbort = (): void => {
|
|
233
259
|
void response?.body?.cancel().catch(() => undefined);
|
|
@@ -245,7 +271,9 @@ async function runFetch<TResult>(
|
|
|
245
271
|
try {
|
|
246
272
|
return await Promise.race([
|
|
247
273
|
(async () => {
|
|
248
|
-
|
|
274
|
+
const fetchImplementation = getFetch(options);
|
|
275
|
+
requestStarted = true;
|
|
276
|
+
response = await fetchImplementation(url, {
|
|
249
277
|
...init,
|
|
250
278
|
redirect: 'error',
|
|
251
279
|
signal: controller.signal,
|
|
@@ -259,13 +287,29 @@ async function runFetch<TResult>(
|
|
|
259
287
|
aborted,
|
|
260
288
|
]);
|
|
261
289
|
} catch (error) {
|
|
262
|
-
|
|
290
|
+
const requestOutcome: TOpenAiChatGptRequestOutcome = requestStarted
|
|
291
|
+
? 'outcomeUnknown'
|
|
292
|
+
: 'notSent';
|
|
293
|
+
const retryAfterMs = response ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined;
|
|
294
|
+
if (error instanceof OpenAiChatGptAuthError) {
|
|
295
|
+
throw new OpenAiChatGptAuthError(error.message, {
|
|
296
|
+
...(error.status !== undefined ? { status: error.status }
|
|
297
|
+
: response ? { status: response.status } : {}),
|
|
298
|
+
...(error.body !== undefined ? { body: error.body } : {}),
|
|
299
|
+
requestOutcome,
|
|
300
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
263
303
|
if (!usesSafeErrors(options)) throw error;
|
|
264
|
-
throw
|
|
304
|
+
throw new OpenAiChatGptAuthError(
|
|
265
305
|
options.signal?.aborted
|
|
266
306
|
? 'OpenAI ChatGPT authentication was aborted.'
|
|
267
307
|
: 'OpenAI ChatGPT authentication request failed.',
|
|
268
|
-
|
|
308
|
+
{
|
|
309
|
+
requestOutcome,
|
|
310
|
+
...(response ? { status: response.status } : {}),
|
|
311
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
312
|
+
},
|
|
269
313
|
);
|
|
270
314
|
} finally {
|
|
271
315
|
clearTimeout(timer);
|
|
@@ -370,7 +414,9 @@ export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenI
|
|
|
370
414
|
chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id),
|
|
371
415
|
chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
|
|
372
416
|
chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
|
|
373
|
-
expiresAt: expiresAtSeconds
|
|
417
|
+
expiresAt: expiresAtSeconds !== undefined
|
|
418
|
+
? new Date(expiresAtSeconds * 1000).toISOString()
|
|
419
|
+
: undefined,
|
|
374
420
|
rawJwt: token,
|
|
375
421
|
};
|
|
376
422
|
}
|
|
@@ -760,16 +806,40 @@ export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptA
|
|
|
760
806
|
baseURL: string;
|
|
761
807
|
headers: Record<string, string>;
|
|
762
808
|
} {
|
|
763
|
-
const
|
|
809
|
+
const currentTokenInfo = parseOpenAiChatGptTokenInfo(credentials.accessToken);
|
|
810
|
+
if (
|
|
811
|
+
currentTokenInfo.expiresAt === undefined
|
|
812
|
+
|| Date.parse(currentTokenInfo.expiresAt) <= Date.now()
|
|
813
|
+
) {
|
|
814
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT access token has no valid future expiry.');
|
|
815
|
+
}
|
|
816
|
+
const tokenAccountId = currentTokenInfo.chatgptAccountId;
|
|
764
817
|
if (
|
|
765
818
|
credentials.accountId !== undefined
|
|
766
|
-
&& tokenAccountId !== undefined
|
|
767
819
|
&& credentials.accountId !== tokenAccountId
|
|
768
820
|
) {
|
|
769
821
|
throw new OpenAiChatGptAuthError('OpenAI ChatGPT account ID does not match the token claim.');
|
|
770
822
|
}
|
|
771
|
-
|
|
772
|
-
|
|
823
|
+
if (
|
|
824
|
+
credentials.tokenInfo !== undefined
|
|
825
|
+
&& (
|
|
826
|
+
credentials.tokenInfo.chatgptAccountId !== tokenAccountId
|
|
827
|
+
|| credentials.tokenInfo.chatgptAccountIsFedramp !== currentTokenInfo.chatgptAccountIsFedramp
|
|
828
|
+
)
|
|
829
|
+
) {
|
|
830
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT token information does not match the current access token.');
|
|
831
|
+
}
|
|
832
|
+
if (
|
|
833
|
+
credentials.isFedrampAccount !== undefined
|
|
834
|
+
&& (
|
|
835
|
+
typeof credentials.isFedrampAccount !== 'boolean'
|
|
836
|
+
|| credentials.isFedrampAccount !== currentTokenInfo.chatgptAccountIsFedramp
|
|
837
|
+
)
|
|
838
|
+
) {
|
|
839
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT FedRAMP status does not match the token claim.');
|
|
840
|
+
}
|
|
841
|
+
const accountId = tokenAccountId;
|
|
842
|
+
const isFedrampAccount = currentTokenInfo.chatgptAccountIsFedramp;
|
|
773
843
|
const headers: Record<string, string> = {
|
|
774
844
|
originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
|
|
775
845
|
};
|