@modelprofile.com/flexharness-providers 7.0.1 → 8.1.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.
@@ -3,6 +3,7 @@ import * as plugins from './plugins.js';
3
3
  import {
4
4
  OPENAI_CHATGPT_CODEX_BASE_URL,
5
5
  OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
6
+ OpenAiChatGptAuthError,
6
7
  completeOpenAiChatGptDeviceCodeLogin,
7
8
  createOpenAiChatGptBrowserAuthorization,
8
9
  exchangeOpenAiChatGptBrowserAuthorizationCode,
@@ -29,6 +30,7 @@ import type {
29
30
  ISmartAiProviderOperationOptions,
30
31
  ISmartAiProviderRateLimitDetails,
31
32
  ISmartAiProviderRateLimitWindow,
33
+ TOpenAiChatGptReadCredential,
32
34
  TSmartAiProviderCancelStatus,
33
35
  TSmartAiProviderAccountRateLimitsResult,
34
36
  TSmartAiProviderCredential,
@@ -183,6 +185,46 @@ const toTokenData = (credentialArg: IOpenAiChatGptOAuthCredential): IOpenAiChatG
183
185
  };
184
186
  };
185
187
 
188
+ interface IOpenAiChatGptReadAccess {
189
+ accessToken: string;
190
+ accountId?: string;
191
+ isFedrampAccount: boolean;
192
+ }
193
+
194
+ const toReadAccess = (
195
+ credentialArg: TOpenAiChatGptReadCredential,
196
+ ): IOpenAiChatGptReadAccess => {
197
+ if (!isRecord(credentialArg)) throw createSmartAiProviderError('CREDENTIAL_INVALID');
198
+ if (credentialArg.kind === 'chatgptOAuth') {
199
+ const tokenData = toTokenData(credentialArg);
200
+ return {
201
+ accessToken: tokenData.accessToken,
202
+ accountId: tokenData.tokenInfo.chatgptAccountId,
203
+ isFedrampAccount: tokenData.tokenInfo.chatgptAccountIsFedramp,
204
+ };
205
+ }
206
+ const access = credentialArg;
207
+ if (
208
+ access.kind !== 'chatgptAccess'
209
+ || access.providerId !== 'openai'
210
+ || typeof access.accessToken !== 'string'
211
+ || access.accessToken.length === 0
212
+ || plugins.Buffer.byteLength(access.accessToken, 'utf8') > maximumHeaderBytes
213
+ || typeof access.accountId !== 'string'
214
+ || access.accountId.length === 0
215
+ || plugins.Buffer.byteLength(access.accountId, 'utf8') > maximumHeaderBytes
216
+ || typeof access.isFedrampAccount !== 'boolean'
217
+ || Object.keys(access).length !== 5
218
+ ) {
219
+ throw createSmartAiProviderError('CREDENTIAL_INVALID');
220
+ }
221
+ return {
222
+ accessToken: access.accessToken,
223
+ accountId: access.accountId,
224
+ isFedrampAccount: access.isFedrampAccount,
225
+ };
226
+ };
227
+
186
228
  const toCredential = (
187
229
  tokenDataArg: IOpenAiChatGptTokenData,
188
230
  lastRefreshAtArg?: string,
@@ -849,6 +891,7 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
849
891
  success: false,
850
892
  credential: credentialArg,
851
893
  errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
894
+ requestOutcome: 'notSent',
852
895
  };
853
896
  waitOperation.cleanup();
854
897
  return failure;
@@ -888,6 +931,7 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
888
931
  success: false,
889
932
  credential: credentialArg,
890
933
  errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
934
+ requestOutcome: 'outcomeUnknown',
891
935
  });
892
936
  waitOperation.signal.addEventListener('abort', onAbort, { once: true });
893
937
  });
@@ -914,7 +958,12 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
914
958
  const tokenChanged = refreshed.accessToken !== input.accessToken
915
959
  || refreshed.refreshToken !== input.refreshToken;
916
960
  if (!tokenChanged || !refreshed.idToken) {
917
- return { success: false, credential: credentialArg, errorCode: 'REFRESH_FAILED' };
961
+ return {
962
+ success: false,
963
+ credential: credentialArg,
964
+ errorCode: 'REFRESH_FAILED',
965
+ requestOutcome: 'outcomeUnknown',
966
+ };
918
967
  }
919
968
  const previous = credentialArg.lastRefreshAt === undefined
920
969
  ? 0
@@ -929,6 +978,15 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
929
978
  success: false,
930
979
  credential: credentialArg,
931
980
  errorCode: classifyOperationError(errorArg, operation, 'REFRESH_FAILED'),
981
+ requestOutcome: errorArg instanceof OpenAiChatGptAuthError
982
+ ? errorArg.requestOutcome ?? 'outcomeUnknown'
983
+ : 'outcomeUnknown',
984
+ ...(errorArg instanceof OpenAiChatGptAuthError && errorArg.status !== undefined
985
+ ? { responseStatus: errorArg.status }
986
+ : {}),
987
+ ...(errorArg instanceof OpenAiChatGptAuthError && errorArg.retryAfterMs !== undefined
988
+ ? { retryAfterMs: errorArg.retryAfterMs }
989
+ : {}),
932
990
  };
933
991
  } finally {
934
992
  this.operations.delete(operation.controller);
@@ -937,11 +995,10 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
937
995
  }
938
996
 
939
997
  private async fetchCatalog(
940
- credentialArg: IOpenAiChatGptOAuthCredential,
998
+ accessArg: IOpenAiChatGptReadAccess,
941
999
  signalArg: AbortSignal,
942
1000
  ): Promise<ISmartAiProviderModel[]> {
943
- const tokenData = toTokenData(credentialArg);
944
- const accountId = tokenData.tokenInfo.chatgptAccountId;
1001
+ const accountId = accessArg.accountId;
945
1002
  const url = `${OPENAI_CHATGPT_CODEX_BASE_URL}/models?client_version=${encodeURIComponent(defaultClientVersion)}`;
946
1003
  let response: Response;
947
1004
  let onAbort!: () => void;
@@ -952,9 +1009,9 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
952
1009
  signal: signalArg,
953
1010
  headers: {
954
1011
  Accept: 'application/json',
955
- Authorization: `Bearer ${credentialArg.accessToken}`,
1012
+ Authorization: `Bearer ${accessArg.accessToken}`,
956
1013
  ...(accountId ? { 'ChatGPT-Account-ID': accountId } : {}),
957
- ...(tokenData.tokenInfo.chatgptAccountIsFedramp ? { 'X-OpenAI-Fedramp': 'true' } : {}),
1014
+ ...(accessArg.isFedrampAccount ? { 'X-OpenAI-Fedramp': 'true' } : {}),
958
1015
  originator: this.originator,
959
1016
  version: defaultClientVersion,
960
1017
  },
@@ -1023,18 +1080,18 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
1023
1080
  }
1024
1081
 
1025
1082
  public async listModels(
1026
- credentialArg: IOpenAiChatGptOAuthCredential,
1083
+ credentialArg: TOpenAiChatGptReadCredential,
1027
1084
  optionsArg: ISmartAiProviderModelListOptions = {},
1028
1085
  ): Promise<TSmartAiProviderModelListResult> {
1029
1086
  this.requireOpen();
1030
- toTokenData(credentialArg);
1087
+ const access = toReadAccess(credentialArg);
1031
1088
  const options = normalizeModelListOptions(optionsArg);
1032
1089
  const operation = createOperation(options, defaultOperationTimeoutMs);
1033
1090
  this.operations.add(operation.controller);
1034
1091
  try {
1035
1092
  let catalog: ISmartAiProviderModel[];
1036
1093
  try {
1037
- catalog = await this.fetchCatalog(credentialArg, operation.signal);
1094
+ catalog = await this.fetchCatalog(access, operation.signal);
1038
1095
  } catch (errorArg) {
1039
1096
  if (!(errorArg instanceof ModelHttpError) || errorArg.status !== 401) throw errorArg;
1040
1097
  return { success: false, errorCode: 'CREDENTIAL_REFRESH_REQUIRED' };
@@ -1084,11 +1141,10 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
1084
1141
  }
1085
1142
 
1086
1143
  private async fetchAccountRateLimits(
1087
- credentialArg: IOpenAiChatGptOAuthCredential,
1144
+ accessArg: IOpenAiChatGptReadAccess,
1088
1145
  signalArg: AbortSignal,
1089
1146
  ): Promise<ISmartAiProviderAccountRateLimits> {
1090
- const tokenData = toTokenData(credentialArg);
1091
- const accountId = tokenData.tokenInfo.chatgptAccountId;
1147
+ const accountId = accessArg.accountId;
1092
1148
  let response: Response;
1093
1149
  let onAbort!: () => void;
1094
1150
  try {
@@ -1098,9 +1154,9 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
1098
1154
  signal: signalArg,
1099
1155
  headers: {
1100
1156
  Accept: 'application/json',
1101
- Authorization: `Bearer ${credentialArg.accessToken}`,
1157
+ Authorization: `Bearer ${accessArg.accessToken}`,
1102
1158
  ...(accountId ? { 'ChatGPT-Account-Id': accountId } : {}),
1103
- ...(tokenData.tokenInfo.chatgptAccountIsFedramp ? { 'X-OpenAI-Fedramp': 'true' } : {}),
1159
+ ...(accessArg.isFedrampAccount ? { 'X-OpenAI-Fedramp': 'true' } : {}),
1104
1160
  'User-Agent': this.originator,
1105
1161
  },
1106
1162
  });
@@ -1130,16 +1186,16 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
1130
1186
  }
1131
1187
 
1132
1188
  public async getAccountRateLimits(
1133
- credentialArg: IOpenAiChatGptOAuthCredential,
1189
+ credentialArg: TOpenAiChatGptReadCredential,
1134
1190
  optionsArg: ISmartAiProviderOperationOptions = {},
1135
1191
  ): Promise<TSmartAiProviderAccountRateLimitsResult> {
1136
1192
  this.requireOpen();
1137
- toTokenData(credentialArg);
1193
+ const access = toReadAccess(credentialArg);
1138
1194
  const operation = createOperation(optionsArg, defaultOperationTimeoutMs);
1139
1195
  this.operations.add(operation.controller);
1140
1196
  try {
1141
1197
  try {
1142
- const rateLimits = await this.fetchAccountRateLimits(credentialArg, operation.signal);
1198
+ const rateLimits = await this.fetchAccountRateLimits(access, operation.signal);
1143
1199
  return { success: true, rateLimits };
1144
1200
  } catch (errorArg) {
1145
1201
  if (!(errorArg instanceof AccountRateLimitsHttpError) || errorArg.status !== 401) {
@@ -1,12 +1,4 @@
1
- import type { IOpenAiModelConnection } from '@modelprofile.com/flexharness-providers/openai';
2
- import type { IOpenAiChatGptAuthCredentials } from './interfaces.js';
3
- import { createOpenAiChatGptProviderSettings } from './smartai.auth.openai.js';
4
- import { createOpenAiChatGptInstructionsMiddleware } from './smartai.middleware.openai.js';
5
1
  export * from './interfaces.js';
6
2
  export * from './smartai.auth.openai.js';
3
+ export * from './smartai.connection.openai.js';
7
4
  export * from './smartai.middleware.openai.js';
8
-
9
- export const createOpenAiChatGptModelConnection = (credentials: IOpenAiChatGptAuthCredentials): IOpenAiModelConnection => ({
10
- settings: createOpenAiChatGptProviderSettings(credentials),
11
- middleware: createOpenAiChatGptInstructionsMiddleware(),
12
- });
@@ -13,10 +13,42 @@ export interface IOpenAiChatGptAuthCredentials {
13
13
  refreshToken?: string;
14
14
  idToken?: string;
15
15
  accountId?: string;
16
+ /** Verified from the current access token when tokenInfo is not supplied. */
17
+ isFedrampAccount?: boolean;
16
18
  tokenInfo?: IOpenAiChatGptTokenInfo;
17
19
  originator?: string;
18
20
  }
19
21
 
22
+ export interface IOpenAiChatGptAccessIdentity {
23
+ /** Exact workspace claim expected on every resolved access token. */
24
+ readonly accountId: string | undefined;
25
+ /** Exact residency claim expected on every resolved access token. */
26
+ readonly isFedrampAccount: boolean;
27
+ }
28
+
29
+ export interface IOpenAiChatGptAccessRequest {
30
+ /** The provider request signal. A resolver should pass it through to its authority. */
31
+ readonly signal: AbortSignal;
32
+ }
33
+
34
+ /** Access-only result. Refresh and ID tokens are not part of the request-time contract. */
35
+ export interface IOpenAiChatGptResolvedAccess {
36
+ readonly accessToken: string;
37
+ }
38
+
39
+ export interface IOpenAiChatGptDynamicModelConnectionOptions {
40
+ /** Pins the account and residency boundary for the lifetime of the connection. */
41
+ readonly expectedIdentity: IOpenAiChatGptAccessIdentity;
42
+ /** Stable provider originator header. Defaults to `smartai`. */
43
+ readonly originator?: string;
44
+ /** Resolves current access immediately before every provider network request. */
45
+ readonly resolveAccess: (
46
+ request: IOpenAiChatGptAccessRequest,
47
+ ) => Promise<IOpenAiChatGptResolvedAccess>;
48
+ /** Optional underlying transport. Defaults to the current global fetch. */
49
+ readonly fetch?: typeof fetch;
50
+ }
51
+
20
52
  export interface IOpenAiChatGptTokenData extends IOpenAiChatGptAuthCredentials {
21
53
  refreshToken: string;
22
54
  tokenInfo: IOpenAiChatGptTokenInfo;
@@ -37,6 +69,9 @@ export interface IOpenAiChatGptAuthOptions {
37
69
  safeErrors?: boolean;
38
70
  }
39
71
 
72
+ /** A failed refresh is replayable only when no request was handed to fetch. */
73
+ export type TOpenAiChatGptRequestOutcome = 'notSent' | 'outcomeUnknown';
74
+
40
75
  export interface IOpenAiChatGptBrowserAuthorizationOptions extends IOpenAiChatGptAuthOptions {
41
76
  redirectUri: string;
42
77
  originator?: string;
@@ -86,4 +121,3 @@ export interface IOpenAiChatGptDeviceCodePollOptions extends IOpenAiChatGptAuthO
86
121
  export interface IOpenAiChatGptCompleteDeviceCodeOptions extends IOpenAiChatGptDeviceCodePollOptions {
87
122
  forcedChatGptWorkspaceId?: string;
88
123
  }
89
-
@@ -11,6 +11,26 @@ 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
+
26
+ Use `createOpenAiChatGptDynamicModelConnection()` for delegated access owned by
27
+ an external authority. Pin its exact account and FedRAMP identity, then provide
28
+ an async `resolveAccess({ signal })` callback. The connection resolves and
29
+ validates current access immediately before every provider HTTP request, so
30
+ multi-step tool runs do not retain the token that admitted the run. Resolution,
31
+ identity, expiry, endpoint, redirect, and abort failures stop before the
32
+ underlying transport is called.
33
+
14
34
  ## Issue Reporting and Security
15
35
 
16
36
  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
  };
@@ -0,0 +1,143 @@
1
+ import type { IOpenAiModelConnection } from '@modelprofile.com/flexharness-providers/openai';
2
+ import type {
3
+ IOpenAiChatGptAuthCredentials,
4
+ IOpenAiChatGptDynamicModelConnectionOptions,
5
+ } from './interfaces.js';
6
+ import {
7
+ createOpenAiChatGptProviderSettings,
8
+ OPENAI_CHATGPT_CODEX_BASE_URL,
9
+ OpenAiChatGptAuthError,
10
+ } from './smartai.auth.openai.js';
11
+ import { createOpenAiChatGptInstructionsMiddleware } from './smartai.middleware.openai.js';
12
+
13
+ const codexBaseUrl = new URL(OPENAI_CHATGPT_CODEX_BASE_URL);
14
+
15
+ function assertCodexRequestUrl(normalizedUrlArg: string): void {
16
+ const url = new URL(normalizedUrlArg);
17
+ const isCodexPath = url.pathname === codexBaseUrl.pathname
18
+ || url.pathname.startsWith(`${codexBaseUrl.pathname}/`);
19
+ if (
20
+ url.origin !== codexBaseUrl.origin
21
+ || url.username !== ''
22
+ || url.password !== ''
23
+ || !isCodexPath
24
+ ) {
25
+ throw new OpenAiChatGptAuthError('OpenAI ChatGPT inference URL is outside the Codex endpoint.');
26
+ }
27
+ }
28
+
29
+ function requireExpectedIdentity(optionsArg: IOpenAiChatGptDynamicModelConnectionOptions): {
30
+ accountId: string | undefined;
31
+ isFedrampAccount: boolean;
32
+ } {
33
+ const expectedIdentity = optionsArg.expectedIdentity;
34
+ if (
35
+ !expectedIdentity
36
+ || (
37
+ expectedIdentity.accountId !== undefined
38
+ && (typeof expectedIdentity.accountId !== 'string' || expectedIdentity.accountId.length === 0)
39
+ )
40
+ || typeof expectedIdentity.isFedrampAccount !== 'boolean'
41
+ ) {
42
+ throw new OpenAiChatGptAuthError('OpenAI ChatGPT expected access identity is invalid.');
43
+ }
44
+ return Object.freeze({
45
+ accountId: expectedIdentity.accountId,
46
+ isFedrampAccount: expectedIdentity.isFedrampAccount,
47
+ });
48
+ }
49
+
50
+ function assertExpectedIdentity(
51
+ settingsArg: ReturnType<typeof createOpenAiChatGptProviderSettings>,
52
+ expectedIdentityArg: Readonly<{
53
+ accountId: string | undefined;
54
+ isFedrampAccount: boolean;
55
+ }>,
56
+ ): void {
57
+ const accountId = settingsArg.headers['ChatGPT-Account-ID'];
58
+ const isFedrampAccount = settingsArg.headers['X-OpenAI-Fedramp'] === 'true';
59
+ if (
60
+ accountId !== expectedIdentityArg.accountId
61
+ || isFedrampAccount !== expectedIdentityArg.isFedrampAccount
62
+ ) {
63
+ throw new OpenAiChatGptAuthError(
64
+ 'OpenAI ChatGPT resolved access identity does not match the connection.',
65
+ );
66
+ }
67
+ }
68
+
69
+ function applyAccessHeaders(
70
+ headersArg: Headers,
71
+ settingsArg: ReturnType<typeof createOpenAiChatGptProviderSettings>,
72
+ ): void {
73
+ headersArg.set('Authorization', `Bearer ${settingsArg.apiKey}`);
74
+ headersArg.set('originator', settingsArg.headers.originator);
75
+
76
+ for (const name of ['ChatGPT-Account-ID', 'X-OpenAI-Fedramp']) {
77
+ const value = settingsArg.headers[name];
78
+ if (value === undefined) {
79
+ headersArg.delete(name);
80
+ } else {
81
+ headersArg.set(name, value);
82
+ }
83
+ }
84
+ }
85
+
86
+ export const createOpenAiChatGptModelConnection = (
87
+ credentialsArg: IOpenAiChatGptAuthCredentials,
88
+ ): IOpenAiModelConnection => ({
89
+ settings: createOpenAiChatGptProviderSettings(credentialsArg),
90
+ middleware: createOpenAiChatGptInstructionsMiddleware(),
91
+ });
92
+
93
+ export const createOpenAiChatGptDynamicModelConnection = (
94
+ optionsArg: IOpenAiChatGptDynamicModelConnectionOptions,
95
+ ): IOpenAiModelConnection => {
96
+ if (!optionsArg || typeof optionsArg.resolveAccess !== 'function') {
97
+ throw new OpenAiChatGptAuthError('OpenAI ChatGPT access resolver is required.');
98
+ }
99
+ const expectedIdentity = requireExpectedIdentity(optionsArg);
100
+ const resolveAccess = optionsArg.resolveAccess;
101
+ const originator = optionsArg.originator;
102
+ const transport = optionsArg.fetch ?? globalThis.fetch;
103
+ if (typeof transport !== 'function') {
104
+ throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT inference.');
105
+ }
106
+
107
+ const fetchWithCurrentAccess: typeof fetch = async (inputArg, initArg) => {
108
+ const request = new Request(inputArg, {
109
+ ...initArg,
110
+ redirect: 'error',
111
+ });
112
+ assertCodexRequestUrl(request.url);
113
+ request.signal.throwIfAborted();
114
+
115
+ const access = await resolveAccess({ signal: request.signal });
116
+ request.signal.throwIfAborted();
117
+ const settings = createOpenAiChatGptProviderSettings({
118
+ accessToken: access.accessToken,
119
+ originator,
120
+ });
121
+ if (settings.baseURL !== OPENAI_CHATGPT_CODEX_BASE_URL) {
122
+ throw new OpenAiChatGptAuthError('OpenAI ChatGPT inference endpoint is invalid.');
123
+ }
124
+ assertExpectedIdentity(settings, expectedIdentity);
125
+
126
+ const headers = new Headers(request.headers);
127
+ applyAccessHeaders(headers, settings);
128
+ const authenticatedRequest = new Request(request, {
129
+ headers,
130
+ });
131
+ authenticatedRequest.signal.throwIfAborted();
132
+ return transport(authenticatedRequest);
133
+ };
134
+
135
+ return {
136
+ settings: {
137
+ apiKey: '',
138
+ baseURL: OPENAI_CHATGPT_CODEX_BASE_URL,
139
+ fetch: fetchWithCurrentAccess,
140
+ },
141
+ middleware: createOpenAiChatGptInstructionsMiddleware(),
142
+ };
143
+ };
@@ -11,6 +11,11 @@ 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
+ ChatGPT connections come from the `/auth` subpath. Use the dynamic connection
15
+ factory for delegated authority access so every provider HTTP request resolves
16
+ a current token while the connection retains its pinned account and residency
17
+ identity.
18
+
14
19
  ## Issue Reporting and Security
15
20
 
16
21
  Report issues and security concerns through [community.foss.global](https://community.foss.global).
@@ -36,7 +36,11 @@ Provider SDKs are installed together. Authentication, file access and vendor med
36
36
  are separate entrypoints and are not imported by the provider factory root. For
37
37
  ChatGPT authentication, pass
38
38
  `connection: createOpenAiChatGptModelConnection(credentials)` from `/auth` to the
39
- OpenAI model options. API-key inference needs no authentication setup.
39
+ OpenAI model options. Delegated runtimes use
40
+ `createOpenAiChatGptDynamicModelConnection({ expectedIdentity, resolveAccess })`
41
+ instead; it resolves current access at every provider HTTP request while keeping
42
+ account and residency identity fixed for the connection. API-key inference
43
+ needs no authentication setup.
40
44
 
41
45
  The package includes the applicable OpenAI Codex license and third-party notices.
42
46