@modelprofile.com/flexharness-providers 7.0.1 → 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.
@@ -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
- constructor(message: string, options: { status?: number; body?: string } = {}) {
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
- throwIfAborted(options);
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
- response = await getFetch(options)(url, {
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
- if (error instanceof OpenAiChatGptAuthError) throw error;
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 authError(
304
+ throw new OpenAiChatGptAuthError(
265
305
  options.signal?.aborted
266
306
  ? 'OpenAI ChatGPT authentication was aborted.'
267
307
  : 'OpenAI ChatGPT authentication request failed.',
268
- options,
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 ? new Date(expiresAtSeconds * 1000).toISOString() : undefined,
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 tokenAccountId = credentials.tokenInfo?.chatgptAccountId;
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
- const accountId = tokenAccountId ?? credentials.accountId;
772
- const isFedrampAccount = credentials.tokenInfo?.chatgptAccountIsFedramp === true;
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
  };