openai 7.7.0 → 7.9.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.
Files changed (223) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -30
  3. package/auth/index.d.mts +1 -1
  4. package/auth/index.d.mts.map +1 -1
  5. package/auth/index.d.ts +1 -1
  6. package/auth/index.d.ts.map +1 -1
  7. package/auth/index.js.map +1 -1
  8. package/auth/index.mjs.map +1 -1
  9. package/auth/types.d.mts +9 -4
  10. package/auth/types.d.mts.map +1 -1
  11. package/auth/types.d.ts +9 -4
  12. package/auth/types.d.ts.map +1 -1
  13. package/auth/workload-identity-auth.d.mts +1 -1
  14. package/auth/workload-identity-auth.d.mts.map +1 -1
  15. package/auth/workload-identity-auth.d.ts +1 -1
  16. package/auth/workload-identity-auth.d.ts.map +1 -1
  17. package/auth/workload-identity-auth.js +23 -6
  18. package/auth/workload-identity-auth.js.map +1 -1
  19. package/auth/workload-identity-auth.mjs +23 -6
  20. package/auth/workload-identity-auth.mjs.map +1 -1
  21. package/auth/x509-transport.d.mts +36 -1
  22. package/auth/x509-transport.d.mts.map +1 -1
  23. package/auth/x509-transport.d.ts +36 -1
  24. package/auth/x509-transport.d.ts.map +1 -1
  25. package/auth/x509-transport.js +224 -1
  26. package/auth/x509-transport.js.map +1 -1
  27. package/auth/x509-transport.mjs +222 -1
  28. package/auth/x509-transport.mjs.map +1 -1
  29. package/azure.d.mts +3 -1
  30. package/azure.d.mts.map +1 -1
  31. package/azure.d.ts +3 -1
  32. package/azure.d.ts.map +1 -1
  33. package/azure.js.map +1 -1
  34. package/azure.mjs.map +1 -1
  35. package/bedrock.d.mts +3 -1
  36. package/bedrock.d.mts.map +1 -1
  37. package/bedrock.d.ts +3 -1
  38. package/bedrock.d.ts.map +1 -1
  39. package/bedrock.js.map +1 -1
  40. package/bedrock.mjs.map +1 -1
  41. package/beta/realtime/ws.d.mts.map +1 -1
  42. package/beta/realtime/ws.d.ts.map +1 -1
  43. package/beta/realtime/ws.js +2 -0
  44. package/beta/realtime/ws.js.map +1 -1
  45. package/beta/realtime/ws.mjs +2 -0
  46. package/beta/realtime/ws.mjs.map +1 -1
  47. package/client.d.mts +3 -1
  48. package/client.d.mts.map +1 -1
  49. package/client.d.ts +3 -1
  50. package/client.d.ts.map +1 -1
  51. package/client.js +31 -38
  52. package/client.js.map +1 -1
  53. package/client.mjs +31 -38
  54. package/client.mjs.map +1 -1
  55. package/core/EventEmitter.d.mts.map +1 -1
  56. package/core/EventEmitter.d.ts.map +1 -1
  57. package/core/EventEmitter.js +14 -1
  58. package/core/EventEmitter.js.map +1 -1
  59. package/core/EventEmitter.mjs +14 -1
  60. package/core/EventEmitter.mjs.map +1 -1
  61. package/internal/auth/x509-api-origin.d.mts +5 -0
  62. package/internal/auth/x509-api-origin.d.mts.map +1 -0
  63. package/internal/auth/x509-api-origin.d.ts +5 -0
  64. package/internal/auth/x509-api-origin.d.ts.map +1 -0
  65. package/internal/auth/x509-api-origin.js +28 -0
  66. package/internal/auth/x509-api-origin.js.map +1 -0
  67. package/internal/auth/x509-api-origin.mjs +24 -0
  68. package/internal/auth/x509-api-origin.mjs.map +1 -0
  69. package/internal/auth/x509-credential-options.d.mts +13 -0
  70. package/internal/auth/x509-credential-options.d.mts.map +1 -0
  71. package/internal/auth/x509-credential-options.d.ts +13 -0
  72. package/internal/auth/x509-credential-options.d.ts.map +1 -0
  73. package/internal/auth/x509-credential-options.js +122 -0
  74. package/internal/auth/x509-credential-options.js.map +1 -0
  75. package/internal/auth/x509-credential-options.mjs +118 -0
  76. package/internal/auth/x509-credential-options.mjs.map +1 -0
  77. package/internal/auth/x509-token-exchange.js +1 -1
  78. package/internal/auth/x509-token-exchange.mjs +1 -1
  79. package/internal/auth/x509-transport-capability.d.mts +12 -5
  80. package/internal/auth/x509-transport-capability.d.mts.map +1 -1
  81. package/internal/auth/x509-transport-capability.d.ts +12 -5
  82. package/internal/auth/x509-transport-capability.d.ts.map +1 -1
  83. package/internal/auth/x509-transport-capability.js +6 -5
  84. package/internal/auth/x509-transport-capability.js.map +1 -1
  85. package/internal/auth/x509-transport-capability.mjs +6 -5
  86. package/internal/auth/x509-transport-capability.mjs.map +1 -1
  87. package/internal/auth/x509-transport-registry.d.mts +8 -0
  88. package/internal/auth/x509-transport-registry.d.mts.map +1 -1
  89. package/internal/auth/x509-transport-registry.d.ts +8 -0
  90. package/internal/auth/x509-transport-registry.d.ts.map +1 -1
  91. package/internal/auth/x509-transport-registry.js +1 -1
  92. package/internal/auth/x509-transport-registry.js.map +1 -1
  93. package/internal/auth/x509-transport-registry.mjs +1 -1
  94. package/internal/auth/x509-transport-registry.mjs.map +1 -1
  95. package/internal/auth/x509-transport-state-browser.d.mts +4 -0
  96. package/internal/auth/x509-transport-state-browser.d.mts.map +1 -1
  97. package/internal/auth/x509-transport-state-browser.d.ts +4 -0
  98. package/internal/auth/x509-transport-state-browser.d.ts.map +1 -1
  99. package/internal/auth/x509-transport-state-browser.js +6 -1
  100. package/internal/auth/x509-transport-state-browser.js.map +1 -1
  101. package/internal/auth/x509-transport-state-browser.mjs +5 -0
  102. package/internal/auth/x509-transport-state-browser.mjs.map +1 -1
  103. package/internal/auth/x509-transport-state.cjs +6 -1
  104. package/internal/auth/x509-transport-state.cjs.map +1 -1
  105. package/internal/auth/x509-transport-state.d.cts +4 -0
  106. package/internal/auth/x509-transport-state.d.cts.map +1 -1
  107. package/internal/auth/x509-transport-state.js +36 -0
  108. package/internal/auth/x509-transport-state.mjs +19 -0
  109. package/internal/auth/x509-workload-identity-auth.d.mts +2 -4
  110. package/internal/auth/x509-workload-identity-auth.d.mts.map +1 -1
  111. package/internal/auth/x509-workload-identity-auth.d.ts +2 -4
  112. package/internal/auth/x509-workload-identity-auth.d.ts.map +1 -1
  113. package/internal/auth/x509-workload-identity-auth.js +44 -35
  114. package/internal/auth/x509-workload-identity-auth.js.map +1 -1
  115. package/internal/auth/x509-workload-identity-auth.mjs +40 -30
  116. package/internal/auth/x509-workload-identity-auth.mjs.map +1 -1
  117. package/internal/uploads.js +4 -2
  118. package/internal/uploads.js.map +1 -1
  119. package/internal/uploads.mjs +4 -2
  120. package/internal/uploads.mjs.map +1 -1
  121. package/internal/utils/log.d.mts +6 -0
  122. package/internal/utils/log.d.mts.map +1 -1
  123. package/internal/utils/log.d.ts +6 -0
  124. package/internal/utils/log.d.ts.map +1 -1
  125. package/internal/utils/log.js +72 -11
  126. package/internal/utils/log.js.map +1 -1
  127. package/internal/utils/log.mjs +69 -11
  128. package/internal/utils/log.mjs.map +1 -1
  129. package/lib/EventEmitter.d.mts.map +1 -1
  130. package/lib/EventEmitter.d.ts.map +1 -1
  131. package/lib/EventEmitter.js +14 -1
  132. package/lib/EventEmitter.js.map +1 -1
  133. package/lib/EventEmitter.mjs +14 -1
  134. package/lib/EventEmitter.mjs.map +1 -1
  135. package/package.json +10 -6
  136. package/realtime/ws.d.mts.map +1 -1
  137. package/realtime/ws.d.ts.map +1 -1
  138. package/realtime/ws.js +2 -0
  139. package/realtime/ws.js.map +1 -1
  140. package/realtime/ws.mjs +2 -0
  141. package/realtime/ws.mjs.map +1 -1
  142. package/resources/admin/organization/audit-logs.d.mts +2 -2
  143. package/resources/admin/organization/audit-logs.d.mts.map +1 -1
  144. package/resources/admin/organization/audit-logs.d.ts +2 -2
  145. package/resources/admin/organization/audit-logs.d.ts.map +1 -1
  146. package/resources/admin/organization/usage.d.mts +5 -0
  147. package/resources/admin/organization/usage.d.mts.map +1 -1
  148. package/resources/admin/organization/usage.d.ts +5 -0
  149. package/resources/admin/organization/usage.d.ts.map +1 -1
  150. package/resources/batches.d.mts +3 -3
  151. package/resources/batches.d.ts +3 -3
  152. package/resources/beta/responses/responses.d.mts +11 -23
  153. package/resources/beta/responses/responses.d.mts.map +1 -1
  154. package/resources/beta/responses/responses.d.ts +11 -23
  155. package/resources/beta/responses/responses.d.ts.map +1 -1
  156. package/resources/beta/responses/responses.js.map +1 -1
  157. package/resources/beta/responses/responses.mjs.map +1 -1
  158. package/resources/beta/responses/ws.d.mts.map +1 -1
  159. package/resources/beta/responses/ws.d.ts.map +1 -1
  160. package/resources/beta/responses/ws.js +3 -0
  161. package/resources/beta/responses/ws.js.map +1 -1
  162. package/resources/beta/responses/ws.mjs +3 -0
  163. package/resources/beta/responses/ws.mjs.map +1 -1
  164. package/resources/chat/completions/completions.d.mts +4 -4
  165. package/resources/chat/completions/completions.d.ts +4 -4
  166. package/resources/evals/evals.d.mts +4 -4
  167. package/resources/evals/evals.d.ts +4 -4
  168. package/resources/evals/runs/runs.d.mts +1 -1
  169. package/resources/evals/runs/runs.d.ts +1 -1
  170. package/resources/responses/responses.d.mts +13 -19
  171. package/resources/responses/responses.d.mts.map +1 -1
  172. package/resources/responses/responses.d.ts +13 -19
  173. package/resources/responses/responses.d.ts.map +1 -1
  174. package/resources/responses/responses.js.map +1 -1
  175. package/resources/responses/responses.mjs.map +1 -1
  176. package/resources/responses/ws.d.mts.map +1 -1
  177. package/resources/responses/ws.d.ts.map +1 -1
  178. package/resources/responses/ws.js +3 -0
  179. package/resources/responses/ws.js.map +1 -1
  180. package/resources/responses/ws.mjs +3 -0
  181. package/resources/responses/ws.mjs.map +1 -1
  182. package/resources/shared.d.mts +0 -2
  183. package/resources/shared.d.mts.map +1 -1
  184. package/resources/shared.d.ts +0 -2
  185. package/resources/shared.d.ts.map +1 -1
  186. package/src/_vendor/partial-json-parser/LICENSE +21 -0
  187. package/src/_vendor/partial-json-parser/README.md +11 -1
  188. package/src/auth/index.ts +1 -0
  189. package/src/auth/types.ts +10 -4
  190. package/src/auth/workload-identity-auth.ts +30 -6
  191. package/src/auth/x509-transport.ts +311 -2
  192. package/src/azure.ts +4 -1
  193. package/src/bedrock.ts +4 -1
  194. package/src/beta/realtime/ws.ts +2 -0
  195. package/src/client.ts +39 -34
  196. package/src/core/EventEmitter.ts +13 -1
  197. package/src/internal/auth/x509-api-origin.ts +27 -0
  198. package/src/internal/auth/x509-credential-options.ts +146 -0
  199. package/src/internal/auth/x509-transport-capability.ts +12 -5
  200. package/src/internal/auth/x509-transport-registry.ts +9 -0
  201. package/src/internal/auth/x509-transport-state-browser.ts +7 -0
  202. package/src/internal/auth/x509-transport-state.cts +7 -0
  203. package/src/internal/auth/x509-workload-identity-auth.ts +42 -33
  204. package/src/internal/uploads.ts +4 -2
  205. package/src/internal/utils/log.ts +76 -11
  206. package/src/lib/EventEmitter.ts +13 -1
  207. package/src/realtime/ws.ts +2 -0
  208. package/src/resources/admin/organization/audit-logs.ts +4 -0
  209. package/src/resources/admin/organization/usage.ts +6 -0
  210. package/src/resources/batches.ts +3 -3
  211. package/src/resources/beta/responses/responses.ts +11 -23
  212. package/src/resources/beta/responses/ws.ts +4 -0
  213. package/src/resources/chat/completions/completions.ts +4 -4
  214. package/src/resources/evals/evals.ts +4 -4
  215. package/src/resources/evals/runs/runs.ts +1 -1
  216. package/src/resources/responses/responses.ts +13 -19
  217. package/src/resources/responses/ws.ts +4 -0
  218. package/src/resources/shared.ts +0 -2
  219. package/src/version.ts +1 -1
  220. package/version.d.mts +1 -1
  221. package/version.d.ts +1 -1
  222. package/version.js +1 -1
  223. package/version.mjs +1 -1
package/src/client.ts CHANGED
@@ -17,17 +17,20 @@ import { resolveDataResidency, type DataResidency } from './internal/data-reside
17
17
  export type { DataResidency } from './internal/data-residency';
18
18
  import * as Errors from './core/error';
19
19
  import * as Pagination from './core/pagination';
20
- import type { WorkloadIdentity, X509WorkloadIdentity } from './auth/types';
20
+ import type { WorkloadIdentity, X509Credential, X509WorkloadIdentity } from './auth/types';
21
21
  import { WorkloadIdentityAuth } from './auth/workload-identity-auth';
22
+ import { X509_API_BASE_URL, assertX509APIOrigin } from './internal/auth/x509-api-origin';
22
23
  import {
23
- X509_API_BASE_URL,
24
24
  X509WorkloadIdentityAuth,
25
- assertX509APIOrigin,
26
25
  assertX509RequestOptions,
27
26
  isX509WorkloadIdentity,
28
27
  snapshotX509RequestOptions,
29
28
  } from './internal/auth/x509-workload-identity-auth';
30
29
  import type { X509Transport } from './internal/auth/x509-transport-registry';
30
+ import {
31
+ normalizeX509CredentialOptions,
32
+ prepareX509ClientClone,
33
+ } from './internal/auth/x509-credential-options';
31
34
  import { isTransientX509ConnectionError, markApprovedX509Client } from '#x509-transport-state';
32
35
  import { OAuthError, SubjectTokenProviderError } from './core/error';
33
36
  import {
@@ -258,6 +261,7 @@ import {
258
261
  formatRequestDetails,
259
262
  loggerFor,
260
263
  parseLogLevel,
264
+ redactURL,
261
265
  } from './internal/utils/log';
262
266
  import { isEmptyObj } from './internal/utils/values';
263
267
 
@@ -430,6 +434,9 @@ export interface ClientOptions {
430
434
  /** Approved, frozen Node.js certificate transport required only for X.509 workload identity. */
431
435
  x509Transport?: X509Transport | undefined;
432
436
 
437
+ /** First-class certificate credential created with `fromX509` from `openai/auth/x509-transport`. */
438
+ credential?: X509Credential | undefined;
439
+
433
440
  /**
434
441
  * Configure this client to use a third-party API provider.
435
442
  * Mutually exclusive with top-level authentication and `baseURL` options.
@@ -457,6 +464,7 @@ export class OpenAI {
457
464
  private fetch: Fetch;
458
465
  #encoder: Opts.RequestEncoder;
459
466
  #x509Authentication: X509WorkloadIdentityAuth | undefined;
467
+ #x509Credential: X509Credential | undefined;
460
468
  #x509Fetch: Fetch | undefined;
461
469
  // Preserve an explicit global selection without storing a second routing URL.
462
470
  #explicitDataResidency = false;
@@ -495,6 +503,8 @@ export class OpenAI {
495
503
  * @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
496
504
  */
497
505
  constructor(clientOptions: ClientOptions = {}) {
506
+ const { credential, options: normalizedOptions } = normalizeX509CredentialOptions(clientOptions);
507
+ clientOptions = normalizedOptions;
498
508
  const residencyBaseURL = resolveDataResidency(clientOptions);
499
509
  const provider = clientOptions.provider;
500
510
  const {
@@ -508,6 +518,7 @@ export class OpenAI {
508
518
  webhookSecret = readEnv('OPENAI_WEBHOOK_SECRET') ?? null,
509
519
  workloadIdentity,
510
520
  x509Transport,
521
+ credential: _credential,
511
522
  ...opts
512
523
  } = clientOptions as InternalClientOptions;
513
524
  if (provider) {
@@ -601,7 +612,7 @@ export class OpenAI {
601
612
  this.fetch = options.fetch ?? Shims.getDefaultFetch();
602
613
  this.#encoder = Opts.FallbackEncoder;
603
614
 
604
- const customHeadersEnv = provider ? undefined : readEnv('OPENAI_CUSTOM_HEADERS');
615
+ const customHeadersEnv = provider || credential ? undefined : readEnv('OPENAI_CUSTOM_HEADERS');
605
616
  if (customHeadersEnv) {
606
617
  const parsed: Record<string, string> = {};
607
618
  for (const line of customHeadersEnv.split('\n')) {
@@ -620,6 +631,7 @@ export class OpenAI {
620
631
  const authentication = new X509WorkloadIdentityAuth(x509Identity, x509Transport, organization, project);
621
632
  this._workloadIdentityAuth = authentication;
622
633
  this.#x509Authentication = authentication;
634
+ this.#x509Credential = credential;
623
635
  this.#x509Fetch = authentication.fetch();
624
636
  this.fetch = this.#x509Fetch;
625
637
  markApprovedX509Client(this);
@@ -639,8 +651,6 @@ export class OpenAI {
639
651
  */
640
652
  withOptions(options: Partial<ClientOptions>): this {
641
653
  const residencyBaseURL = resolveDataResidency(options);
642
- const inheritedProvider = this._options.provider;
643
- const provider = options.provider ?? inheritedProvider;
644
654
  const x509Authentication = this.#x509Authentication;
645
655
  const inheritedOptions: ClientOptions = {
646
656
  ...this._options,
@@ -659,47 +669,41 @@ export class OpenAI {
659
669
  project: this.project,
660
670
  webhookSecret: this.webhookSecret,
661
671
  };
662
- const currentlyX509 = x509Authentication !== undefined;
663
- const nextIdentity = hasOwn(options, 'workloadIdentity')
664
- ? options.workloadIdentity
665
- : inheritedOptions.workloadIdentity;
666
- const nextX509 = isX509WorkloadIdentity(nextIdentity);
667
- if (currentlyX509 !== nextX509) {
668
- delete inheritedOptions.fetch;
669
- delete inheritedOptions.baseURL;
670
- if (nextX509) {
671
- inheritedOptions.apiKey = null;
672
- } else {
673
- delete inheritedOptions.x509Transport;
674
- }
675
- }
672
+ const { credential, provider } = prepareX509ClientClone(
673
+ inheritedOptions,
674
+ options,
675
+ this.#x509Credential,
676
+ x509Authentication !== undefined,
677
+ );
676
678
  if (residencyBaseURL !== undefined) {
677
679
  delete inheritedOptions.baseURL;
678
680
  }
679
- if (provider) {
680
- delete inheritedOptions.apiKey;
681
- delete inheritedOptions.adminAPIKey;
682
- delete inheritedOptions.workloadIdentity;
683
- delete inheritedOptions.x509Transport;
684
- delete inheritedOptions.baseURL;
685
- if (provider !== inheritedProvider) {
686
- delete inheritedOptions.organization;
687
- delete inheritedOptions.project;
688
- delete inheritedOptions.defaultHeaders;
689
- }
690
- }
691
681
 
692
682
  const clientOptions: InternalClientOptions = {
693
683
  ...inheritedOptions,
694
684
  ...options,
685
+ credential,
695
686
  provider,
696
687
  [inheritedDataResidencySelection]:
697
688
  this.#explicitDataResidency &&
698
689
  residencyBaseURL === undefined &&
699
690
  !hasOwn(options, 'baseURL') &&
691
+ options.credential === undefined &&
700
692
  !provider,
701
693
  };
702
694
  const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)(clientOptions);
695
+ if (provider && new URL(client.baseURL).origin !== new URL(this.baseURL).origin) {
696
+ Object.assign(client._options, {
697
+ defaultHeaders: options.defaultHeaders,
698
+ defaultQuery: options.defaultQuery,
699
+ fetchOptions: options.fetchOptions,
700
+ fetch: options.fetch,
701
+ });
702
+ client.fetchOptions = options.fetchOptions;
703
+ client.fetch = options.fetch ?? Shims.getDefaultFetch();
704
+ client.organization = options.organization ?? null;
705
+ client.project = options.project ?? null;
706
+ }
703
707
  if (
704
708
  this.#x509Authentication &&
705
709
  client.#x509Authentication &&
@@ -1151,9 +1155,10 @@ export class OpenAI {
1151
1155
  retriesRemaining = maxRetries;
1152
1156
  }
1153
1157
 
1158
+ const x509Authentication = this.#x509Authentication;
1159
+ x509Authentication?.beginRequestPreparation();
1154
1160
  await this.prepareOptions(options);
1155
1161
 
1156
- const x509Authentication = this.#x509Authentication;
1157
1162
  x509Authentication?.beginRequestPlanning();
1158
1163
  let built: { req: FinalizedRequestInit; url: string; timeout: number };
1159
1164
  try {
@@ -1346,7 +1351,7 @@ export class OpenAI {
1346
1351
  .filter(([name]) => name === 'x-request-id')
1347
1352
  .map(([name, value]) => ', ' + name + ': ' + JSON.stringify(value))
1348
1353
  .join('');
1349
- const responseInfo = `[${requestLogID}${retryLogStr}${specialHeaders}] ${req.method} ${url} ${
1354
+ const responseInfo = `[${requestLogID}${retryLogStr}${specialHeaders}] ${req.method} ${redactURL(url)} ${
1350
1355
  response.ok ? 'succeeded' : 'failed'
1351
1356
  } with status ${response.status} in ${headersTime - startTime}ms`;
1352
1357
 
@@ -186,11 +186,20 @@ export class EventEmitter<EventTypes extends Record<string, (...args: any) => an
186
186
  }
187
187
  return !listener.once && !listener.removed;
188
188
  }) as any;
189
+ let listenerThrew = false;
190
+ let firstListenerError: unknown;
189
191
  this.#listenerDispatchDepth += 1;
190
192
  try {
191
193
  for (const registration of listeners as any) {
192
194
  if (!registration.removed) {
193
- registration.listener(...(args as any));
195
+ try {
196
+ registration.listener(...(args as any));
197
+ } catch (error) {
198
+ if (!listenerThrew) {
199
+ listenerThrew = true;
200
+ firstListenerError = error;
201
+ }
202
+ }
194
203
  }
195
204
  }
196
205
  } finally {
@@ -199,6 +208,9 @@ export class EventEmitter<EventTypes extends Record<string, (...args: any) => an
199
208
  this.#cleanupEmittedListeners();
200
209
  }
201
210
  }
211
+ if (listenerThrew) {
212
+ throw firstListenerError;
213
+ }
202
214
  }
203
215
  }
204
216
 
@@ -0,0 +1,27 @@
1
+ import { OpenAIError } from '../../core/error';
2
+ import { isSensitiveQueryParameter } from '../utils/log';
3
+
4
+ /** Sole API authority approved for OpenAI X.509 workload-identity federation. */
5
+ export const X509_API_BASE_URL = 'https://mtls.api.openai.com/v1';
6
+
7
+ /** Validates the enrolled API authority and rejects credential-bearing query parameters. */
8
+ export function assertX509APIOrigin(value: string | URL): URL {
9
+ let target: URL;
10
+ try {
11
+ target = new URL(value);
12
+ } catch {
13
+ throw new OpenAIError('X.509 workload identity requires the approved global mTLS API origin.');
14
+ }
15
+
16
+ if (target.origin !== 'https://mtls.api.openai.com' || target.username || target.password) {
17
+ throw new OpenAIError('X.509 workload identity requires the approved global mTLS API origin.');
18
+ }
19
+ for (const name of target.searchParams.keys()) {
20
+ if (isSensitiveQueryParameter(name)) {
21
+ throw new OpenAIError(
22
+ 'X.509 workload identity cannot send conflicting query authentication credentials.',
23
+ );
24
+ }
25
+ }
26
+ return target;
27
+ }
@@ -0,0 +1,146 @@
1
+ import { OpenAIError } from '../../core/error';
2
+ import type { ClientOptions } from '../../client';
3
+ import type { X509Credential } from '../../auth/types';
4
+ import { hasOwn } from '../utils/values';
5
+ import { isX509WorkloadIdentity } from './x509-workload-identity-auth';
6
+ import type { RegisteredX509Credential } from './x509-transport-registry';
7
+ import { findX509Credential } from '#x509-transport-state';
8
+
9
+ /** Validates one privately registered credential and suppresses ambient legacy authentication. */
10
+ export function normalizeX509CredentialOptions(options: ClientOptions): {
11
+ credential: X509Credential | undefined;
12
+ options: ClientOptions;
13
+ } {
14
+ const { credential } = options;
15
+ if (credential === undefined) {
16
+ return { credential, options };
17
+ }
18
+
19
+ const registered: RegisteredX509Credential | undefined = findX509Credential(credential);
20
+ if (!registered) {
21
+ throw new OpenAIError('An X.509 credential must be created by the SDK authentication helper.');
22
+ }
23
+ const conflicting = (['apiKey', 'adminAPIKey', 'workloadIdentity', 'x509Transport'] as const).filter(
24
+ (name) => {
25
+ const value = options[name];
26
+ return value !== null && value !== undefined;
27
+ },
28
+ );
29
+ if (conflicting.length > 0) {
30
+ throw new OpenAIError(
31
+ `The \`credential\` option cannot be combined with ${conflicting.map((name) => `\`${name}\``).join(', ')}.`,
32
+ );
33
+ }
34
+
35
+ return {
36
+ credential,
37
+ options: {
38
+ ...options,
39
+ apiKey: null,
40
+ adminAPIKey: null,
41
+ baseURL: options.baseURL ?? null,
42
+ organization: options.organization ?? null,
43
+ project: options.project ?? null,
44
+ workloadIdentity: registered.identity,
45
+ x509Transport: registered.transport,
46
+ },
47
+ };
48
+ }
49
+
50
+ /** Distinguishes explicitly supplied ordinary credentials from nullish inheritance. */
51
+ function overridesOrdinaryAuthentication({ apiKey, adminAPIKey }: Partial<ClientOptions>): boolean {
52
+ return (apiKey !== null && apiKey !== undefined) || (adminAPIKey !== null && adminAPIKey !== undefined);
53
+ }
54
+
55
+ /** Keeps origin-bound configuration with its owning provider during a client clone. */
56
+ function prepareProviderClone(
57
+ inherited: ClientOptions,
58
+ overrides: Partial<ClientOptions>,
59
+ ): ClientOptions['provider'] {
60
+ const inheritedProvider = inherited.provider;
61
+ const replacingProvider = overrides.credential ?? overrides.workloadIdentity;
62
+ const provider = overrides.provider ?? (replacingProvider ? undefined : inheritedProvider);
63
+
64
+ if (provider !== inheritedProvider) {
65
+ delete inherited.baseURL;
66
+ delete inherited.organization;
67
+ delete inherited.project;
68
+ delete inherited.defaultHeaders;
69
+ delete inherited.defaultQuery;
70
+ delete inherited.fetchOptions;
71
+ delete inherited.fetch;
72
+ }
73
+ if (provider) {
74
+ delete inherited.apiKey;
75
+ delete inherited.adminAPIKey;
76
+ delete inherited.credential;
77
+ delete inherited.workloadIdentity;
78
+ delete inherited.x509Transport;
79
+ delete inherited.baseURL;
80
+ }
81
+ return provider;
82
+ }
83
+
84
+ /** Reconciles one client's credential and provider ownership before cloning its options. */
85
+ export function prepareX509ClientClone(
86
+ inherited: ClientOptions,
87
+ overrides: Partial<ClientOptions>,
88
+ credential: X509Credential | undefined,
89
+ currentlyX509: boolean,
90
+ ): { credential: X509Credential | undefined; provider: ClientOptions['provider'] } {
91
+ const nextIdentity = hasOwn(overrides, 'workloadIdentity')
92
+ ? overrides.workloadIdentity
93
+ : inherited.workloadIdentity;
94
+ const dropping =
95
+ credential !== undefined &&
96
+ ((overridesOrdinaryAuthentication(overrides) && overrides.workloadIdentity === undefined) ||
97
+ overrides.provider !== undefined);
98
+ if (credential !== undefined && hasOwn(overrides, 'workloadIdentity')) {
99
+ delete inherited.x509Transport;
100
+ }
101
+ const inheritedCredential =
102
+ credential !== undefined &&
103
+ !dropping &&
104
+ overrides.credential === undefined &&
105
+ !hasOwn(overrides, 'workloadIdentity') &&
106
+ !hasOwn(overrides, 'x509Transport')
107
+ ? credential
108
+ : undefined;
109
+ const nextCredential = overrides.credential === undefined ? inheritedCredential : overrides.credential;
110
+ const nextX509 = nextCredential !== undefined || (!dropping && isX509WorkloadIdentity(nextIdentity));
111
+
112
+ if (currentlyX509 !== nextX509) {
113
+ delete inherited.fetch;
114
+ delete inherited.baseURL;
115
+ delete inherited.organization;
116
+ delete inherited.project;
117
+ delete inherited.defaultHeaders;
118
+ delete inherited.defaultQuery;
119
+ delete inherited.fetchOptions;
120
+ if (nextX509) {
121
+ inherited.apiKey = null;
122
+ } else {
123
+ delete inherited.x509Transport;
124
+ if (dropping) {
125
+ delete inherited.workloadIdentity;
126
+ }
127
+ }
128
+ }
129
+
130
+ if (nextCredential !== undefined) {
131
+ delete inherited.apiKey;
132
+ delete inherited.adminAPIKey;
133
+ delete inherited.workloadIdentity;
134
+ delete inherited.x509Transport;
135
+ inherited.credential = nextCredential;
136
+ if (overrides.credential !== undefined) {
137
+ delete inherited.organization;
138
+ delete inherited.project;
139
+ delete inherited.defaultHeaders;
140
+ delete inherited.defaultQuery;
141
+ delete inherited.fetchOptions;
142
+ }
143
+ }
144
+
145
+ return { credential: nextCredential, provider: prepareProviderClone(inherited, overrides) };
146
+ }
@@ -15,7 +15,13 @@ export interface X509TransportOptions {
15
15
  /** X.509 transport currently supports genuine Node.js runtimes only. */
16
16
  runtime: 'node';
17
17
 
18
- /** Caller-owned Undici Agent or ProxyAgent; the SDK never closes or inspects it. */
18
+ /**
19
+ * Caller-owned Undici Agent or ProxyAgent; the SDK never closes or inspects it.
20
+ *
21
+ * The application attests that its dispatcher, factories, TLS verification,
22
+ * certificate selection, and CONNECT proxy configuration are trustworthy.
23
+ * Use `fromX509` when the SDK should own and enforce transport configuration.
24
+ */
19
25
  dispatcher: Agent | ProxyAgent;
20
26
 
21
27
  /** Attests that the dispatcher uses one static workload-certificate identity. */
@@ -150,10 +156,11 @@ function assertConnectProxySupport(): void {
150
156
  * `certificateIdentity: 'static'` is an application attestation: the SDK does
151
157
  * not inspect certificates, private dispatcher internals, callbacks, or TLS
152
158
  * options and cannot cryptographically prove certificate selection. Configure
153
- * one static identity without custom dispatcher factories. For HTTPS CONNECT,
154
- * independently configure `proxyTls` and `requestTls` so workload credentials
155
- * never reach the proxy. Rotation requires creating a fresh dispatcher and
156
- * capability; the application remains responsible for draining the old one.
159
+ * trusted dispatcher factories, verified target and proxy TLS, one static
160
+ * certificate identity, and independently scoped CONNECT credentials.
161
+ * Prefer the SDK-owned `fromX509` credential when these guarantees should be
162
+ * enforced at construction. Rotation requires a fresh caller-owned dispatcher
163
+ * and capability; the application remains responsible for draining it.
157
164
  *
158
165
  * This Node-only preview entrypoint requires the optional `undici` peer at
159
166
  * version 5.2.0 or later. CONNECT proxy modes require version 5.5.1 or
@@ -1,5 +1,6 @@
1
1
  import { OpenAIError } from '../../core/error';
2
2
  import type { NullableHeaders } from '../headers';
3
+ import type { X509WorkloadIdentity } from '../../auth/types';
3
4
  import type { ReadableStream } from '../shim-types';
4
5
  import type { MergedRequestInit } from '../types';
5
6
  import { findRegisteredX509Transport } from '#x509-transport-state';
@@ -49,6 +50,12 @@ export interface X509Transport {
49
50
  readonly [x509TransportBrand]: true;
50
51
  }
51
52
 
53
+ /** Immutable selectors and transport privately registered for an SDK-owned credential. */
54
+ export interface RegisteredX509Credential {
55
+ readonly identity: X509WorkloadIdentity;
56
+ readonly transport: X509Transport;
57
+ }
58
+
52
59
  /** Validated short-lived token exchanged using a registered certificate identity. */
53
60
  export interface X509ExchangedToken {
54
61
  /** Header-safe, in-memory OpenAI bearer credential. */
@@ -63,6 +70,8 @@ export interface X509RequestScope {
63
70
  wallStartedAt: number;
64
71
  monotonicStartedAt: number;
65
72
  deadlineArmed?: boolean;
73
+ preparationStartedAt?: number;
74
+ preparationWallStartedAt?: number;
66
75
  request?: { signal: AbortSignal | null | undefined; timeout: number; fetchOptions: MergedRequestInit };
67
76
  phase?: 'planning' | 'authorizing';
68
77
  effectiveSignal?: AbortSignal;
@@ -4,6 +4,7 @@ const transientX509ConnectionErrors = new WeakSet();
4
4
  const retryableX509IssuerErrors = new WeakSet();
5
5
  const approvedX509Clients = new WeakSet();
6
6
  const approvedX509OAuthErrors = new WeakMap();
7
+ const approvedX509Credentials = new WeakMap();
7
8
 
8
9
  /** Looks up an opaque capability without exposing the registry itself. */
9
10
  export const findRegisteredX509Transport = WeakMap.prototype.get.bind(registeredX509Transports);
@@ -34,3 +35,9 @@ export const rememberX509OAuthError = WeakMap.prototype.set.bind(approvedX509OAu
34
35
 
35
36
  /** Retrieves trusted metadata when public OAuth errors cross module formats. */
36
37
  export const findX509OAuthError = WeakMap.prototype.get.bind(approvedX509OAuthErrors);
38
+
39
+ /** Privately binds SDK-owned credentials without importing Node or optional transport peers. */
40
+ export const rememberX509Credential = WeakMap.prototype.set.bind(approvedX509Credentials);
41
+
42
+ /** Resolves only credentials registered by the optional Node authentication helper. */
43
+ export const findX509Credential = WeakMap.prototype.get.bind(approvedX509Credentials);
@@ -4,6 +4,7 @@ const transientX509ConnectionErrors = new WeakSet();
4
4
  const retryableX509IssuerErrors = new WeakSet();
5
5
  const approvedX509Clients = new WeakSet();
6
6
  const approvedX509OAuthErrors = new WeakMap();
7
+ const approvedX509Credentials = new WeakMap();
7
8
 
8
9
  /** Looks up an opaque capability without exposing the registry itself. */
9
10
  export const findRegisteredX509Transport = WeakMap.prototype.get.bind(registeredX509Transports);
@@ -34,3 +35,9 @@ export const rememberX509OAuthError = WeakMap.prototype.set.bind(approvedX509OAu
34
35
 
35
36
  /** Retrieves trusted OAuth metadata for public cross-module error normalization. */
36
37
  export const findX509OAuthError = WeakMap.prototype.get.bind(approvedX509OAuthErrors);
38
+
39
+ /** Privately binds SDK-owned credentials to their immutable identity and approved transport. */
40
+ export const rememberX509Credential = WeakMap.prototype.set.bind(approvedX509Credentials);
41
+
42
+ /** Resolves only first-class credentials created by the optional Node transport helper. */
43
+ export const findX509Credential = WeakMap.prototype.get.bind(approvedX509Credentials);
@@ -6,7 +6,9 @@ import type { HeadersLike, NullableHeaders } from '../headers';
6
6
  import type { FinalRequestOptions } from '../request-options';
7
7
  import { CancelReadableStream } from '../shims';
8
8
  import type { MergedRequestInit } from '../types';
9
+ import { isSensitiveHeader } from '../utils/log';
9
10
  import { hasOwn } from '../utils/values';
11
+ import { assertX509APIOrigin } from './x509-api-origin';
10
12
  import { resolveX509Transport } from './x509-transport-registry';
11
13
  import {
12
14
  isApprovedX509Client,
@@ -21,10 +23,6 @@ import type {
21
23
  X509Transport,
22
24
  } from './x509-transport-registry';
23
25
 
24
- /** Sole API authority approved for OpenAI X.509 workload-identity federation. */
25
- export const X509_API_BASE_URL = 'https://mtls.api.openai.com/v1';
26
-
27
- const X509_API_ORIGIN = 'https://mtls.api.openai.com';
28
26
  const FORBIDDEN_TRANSPORT_OPTIONS = ['dispatcher', 'agent', 'client', 'tls', 'proxy'];
29
27
  const headerValue = (headers: Headers, name: string): string | null =>
30
28
  Headers.prototype.get.call(headers, name);
@@ -39,12 +37,7 @@ const userAbortError = (signal: AbortSignal): APIUserAbortError => {
39
37
  function assertSafeHeaders(headers: Headers): void {
40
38
  for (const name of Headers.prototype.keys.call(headers)) {
41
39
  const canonical = name.toLowerCase().split('_').join('-');
42
- if (
43
- canonical === 'api-key' ||
44
- canonical === 'x-api-key' ||
45
- canonical === 'proxy-authorization' ||
46
- canonical === 'host'
47
- ) {
40
+ if ((canonical !== 'authorization' && isSensitiveHeader(canonical)) || canonical === 'host') {
48
41
  throw new OpenAIError('X.509 workload identity cannot send conflicting authentication credentials.');
49
42
  }
50
43
  }
@@ -180,21 +173,6 @@ export function assertX509WebSocketSupported(client: unknown): void {
180
173
  }
181
174
  }
182
175
 
183
- /** Rejects every destination outside the sole enrolled, global X.509 API authority. */
184
- export function assertX509APIOrigin(value: string | URL): URL {
185
- let target: URL;
186
- try {
187
- target = new URL(value);
188
- } catch {
189
- throw new OpenAIError('X.509 workload identity requires the approved global mTLS API origin.');
190
- }
191
-
192
- if (target.origin !== X509_API_ORIGIN || target.username || target.password) {
193
- throw new OpenAIError('X.509 workload identity requires the approved global mTLS API origin.');
194
- }
195
- return target;
196
- }
197
-
198
176
  /** Prevents caller options from replacing the immutable transport selected at construction. */
199
177
  export function assertX509FetchOptions(options: MergedRequestInit | RequestInit | undefined): void {
200
178
  if (!options) {
@@ -235,6 +213,7 @@ export class X509WorkloadIdentityAuth {
235
213
  readonly #identityProviderId: string;
236
214
  readonly #serviceAccountId: string;
237
215
  readonly #configuredRefreshBufferMs: number | undefined;
216
+ readonly #configuredRefreshBufferSeconds: number | undefined;
238
217
  readonly #organization: string | null;
239
218
  readonly #project: string | null;
240
219
  readonly #transport: RegisteredX509Transport;
@@ -254,15 +233,32 @@ export class X509WorkloadIdentityAuth {
254
233
  this.#identityProviderId = identity.identityProviderId;
255
234
  this.#serviceAccountId = identity.serviceAccountId;
256
235
  this.#configuredRefreshBufferMs = identity.refreshBufferMs;
236
+ this.#configuredRefreshBufferSeconds = identity.refreshBufferSeconds;
257
237
  this.#organization = organization;
258
238
  this.#project = project;
239
+ if (this.#configuredRefreshBufferMs !== undefined && this.#configuredRefreshBufferSeconds !== undefined) {
240
+ throw new OpenAIError(
241
+ 'X.509 workload identity cannot combine refreshBufferSeconds and refreshBufferMs.',
242
+ );
243
+ }
259
244
  if (
260
245
  this.#configuredRefreshBufferMs !== undefined &&
261
246
  (!Number.isSafeInteger(this.#configuredRefreshBufferMs) || this.#configuredRefreshBufferMs < 0)
262
247
  ) {
263
248
  throw new OpenAIError('X.509 workload identity requires a nonnegative integer refreshBufferMs.');
264
249
  }
265
- this.#refreshBufferMs = this.#configuredRefreshBufferMs ?? DEFAULT_REFRESH_BUFFER_MS;
250
+ if (
251
+ this.#configuredRefreshBufferSeconds !== undefined &&
252
+ (!Number.isSafeInteger(this.#configuredRefreshBufferSeconds) ||
253
+ this.#configuredRefreshBufferSeconds < 0 ||
254
+ !Number.isSafeInteger(this.#configuredRefreshBufferSeconds * 1000))
255
+ ) {
256
+ throw new OpenAIError('X.509 workload identity requires a nonnegative integer refreshBufferSeconds.');
257
+ }
258
+ this.#refreshBufferMs =
259
+ this.#configuredRefreshBufferSeconds === undefined
260
+ ? (this.#configuredRefreshBufferMs ?? DEFAULT_REFRESH_BUFFER_MS)
261
+ : this.#configuredRefreshBufferSeconds * 1000;
266
262
  }
267
263
 
268
264
  /** Reconstructs the immutable selectors captured before caller-owned identity mutation. */
@@ -274,6 +270,9 @@ export class X509WorkloadIdentityAuth {
274
270
  ...(this.#configuredRefreshBufferMs === undefined
275
271
  ? {}
276
272
  : { refreshBufferMs: this.#configuredRefreshBufferMs }),
273
+ ...(this.#configuredRefreshBufferSeconds === undefined
274
+ ? {}
275
+ : { refreshBufferSeconds: this.#configuredRefreshBufferSeconds }),
277
276
  };
278
277
  }
279
278
 
@@ -365,6 +364,15 @@ export class X509WorkloadIdentityAuth {
365
364
  return request;
366
365
  }
367
366
 
367
+ /** Suspends an already-running network budget during retry-local asynchronous preparation. */
368
+ beginRequestPreparation(): void {
369
+ const scope = this.#scope();
370
+ if (scope.deadlineArmed && scope.preparationStartedAt === undefined) {
371
+ scope.preparationStartedAt = performance.now();
372
+ scope.preparationWallStartedAt = Date.now();
373
+ }
374
+ }
375
+
368
376
  /** Begins local request construction without charging protected hook latency to the network. */
369
377
  beginRequestPlanning(): void {
370
378
  this.#scope().phase = 'planning';
@@ -377,6 +385,11 @@ export class X509WorkloadIdentityAuth {
377
385
  scope.wallStartedAt = Date.now();
378
386
  scope.monotonicStartedAt = performance.now();
379
387
  scope.deadlineArmed = true;
388
+ } else if (scope.preparationStartedAt !== undefined) {
389
+ scope.monotonicStartedAt += performance.now() - scope.preparationStartedAt;
390
+ scope.wallStartedAt += Date.now() - (scope.preparationWallStartedAt ?? Date.now());
391
+ delete scope.preparationStartedAt;
392
+ delete scope.preparationWallStartedAt;
380
393
  }
381
394
  }
382
395
 
@@ -550,6 +563,8 @@ export class X509WorkloadIdentityAuth {
550
563
  delete scope.request;
551
564
  delete scope.phase;
552
565
  delete scope.deadlineArmed;
566
+ delete scope.preparationStartedAt;
567
+ delete scope.preparationWallStartedAt;
553
568
  delete scope.effectiveSignal;
554
569
  delete scope.materializedBody;
555
570
  delete scope.apiURL;
@@ -820,13 +835,7 @@ export class X509WorkloadIdentityAuth {
820
835
  'X.509 workload identity cannot override its enrolled organization or project.',
821
836
  );
822
837
  }
823
- if (
824
- canonical === 'authorization' ||
825
- canonical === 'api-key' ||
826
- canonical === 'x-api-key' ||
827
- canonical === 'proxy-authorization' ||
828
- canonical === 'host'
829
- ) {
838
+ if (isSensitiveHeader(canonical) || canonical === 'host') {
830
839
  throw new OpenAIError(
831
840
  'X.509 workload identity cannot use caller-supplied authentication credentials.',
832
841
  );
@@ -348,7 +348,8 @@ const hasStreamingUploadableValue = (value: unknown): boolean => {
348
348
  return value.some(hasStreamingUploadableValue);
349
349
  }
350
350
  if (value && typeof value === 'object' && !isBlob(value) && !(value instanceof Response)) {
351
- for (const k in value) {
351
+ // Own properties only, matching what form encoding serializes; inherited values are never encoded.
352
+ for (const k of Object.keys(value)) {
352
353
  if (hasStreamingUploadableValue((value as Record<string, unknown>)[k])) {
353
354
  return true;
354
355
  }
@@ -365,7 +366,8 @@ const hasUploadableValue = (value: unknown): boolean => {
365
366
  return value.some(hasUploadableValue);
366
367
  }
367
368
  if (value && typeof value === 'object') {
368
- for (const k in value) {
369
+ // Own properties only, matching what form encoding serializes; inherited values are never encoded.
370
+ for (const k of Object.keys(value)) {
369
371
  if (hasUploadableValue((value as any)[k])) {
370
372
  return true;
371
373
  }