Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ const config: JestConfigWithTsJest = {
),
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }],
'^.+\\.cts$': ['@swc/jest', { sourceMaps: 'inline' }],
},
moduleNameMapper: {
'^#x509-transport-state$': '<rootDir>/src/internal/auth/x509-transport-state.cts',
'^openai$': '<rootDir>/src/index.ts',
'^openai/(.*)$': '<rootDir>/src/$1',
},
Expand Down
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,30 @@
"overrides": {
"minimatch": "^9.0.5"
},
"imports": {
"#x509-transport-state": {
"types": "./src/internal/auth/x509-transport-state.cts",
"browser": "./src/internal/auth/x509-transport-state-browser.ts",
"node": "./src/internal/auth/x509-transport-state.cts",
"default": "./src/internal/auth/x509-transport-state-browser.ts"
}
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./internal/auth/x509-transport-capability": null,
"./internal/auth/x509-transport-capability.js": null,
"./internal/auth/x509-transport-capability.mjs": null,
"./internal/auth/x509-transport-registry": null,
"./internal/auth/x509-transport-registry.js": null,
"./internal/auth/x509-transport-registry.mjs": null,
"./internal/auth/x509-transport-state": null,
"./internal/auth/x509-transport-state.cjs": null,
"./internal/auth/x509-transport-state-browser": null,
"./internal/auth/x509-transport-state-browser.js": null,
"./internal/auth/x509-transport-state-browser.mjs": null,
"./*.mjs": {
"default": "./dist/*.mjs"
},
Expand Down
31 changes: 31 additions & 0 deletions scripts/test-packed-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ const packedPackagePath = require('node:path');
tarball,
]);

const privateX509Modules = [
'openai/internal/auth/x509-transport-capability',
'openai/internal/auth/x509-transport-capability.js',
'openai/internal/auth/x509-transport-capability.mjs',
'openai/internal/auth/x509-transport-registry',
'openai/internal/auth/x509-transport-registry.js',
'openai/internal/auth/x509-transport-registry.mjs',
'openai/internal/auth/x509-transport-state',
'openai/internal/auth/x509-transport-state.cjs',
'openai/internal/auth/x509-transport-state-browser',
'openai/internal/auth/x509-transport-state-browser.js',
'openai/internal/auth/x509-transport-state-browser.mjs',
];
const moduleNames = JSON.stringify(privateX509Modules);
run(process.execPath, [
'--input-type=commonjs',
'--eval',
`for (const name of ${moduleNames}) { try { require(name); throw new Error(name + ' is publicly accessible'); } catch (error) { if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error; } }`,
]);
run(process.execPath, [
'--input-type=module',
'--eval',
`for (const name of ${moduleNames}) { try { await import(name); throw new Error(name + ' is publicly accessible'); } catch (error) { if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error; } }`,
]);

const unsupportedDispatcher =
'assert.throws(direct, /Undici 5\\.2\\.0 or later/u); assert.throws(httpConnect, /Undici 5\\.2\\.0 or later/u); assert.throws(httpsConnect, /Undici 5\\.2\\.0 or later/u);';
const unsupportedProxy =
Expand Down Expand Up @@ -466,6 +491,12 @@ const packedPackagePath = require('node:path');
assert.equal(installedPackage.peerDependenciesMeta?.['undici']?.optional, true);
const optionalUndici = path.join(temporaryDirectory, 'node_modules/undici');
assert(!fs.existsSync(optionalUndici), 'Undici must remain optional for ordinary SDK consumers');
run(process.execPath, [
'--conditions=browser',
'--input-type=module',
'--eval',
"import OpenAI from 'openai'; new OpenAI({ apiKey: 'synthetic-browser-api-key', dangerouslyAllowBrowser: true });",
]);
fs.symlinkSync(path.join(root, 'node_modules/undici'), optionalUndici, 'dir');

for (const [inputType, consumer] of [
Expand Down
13 changes: 13 additions & 0 deletions scripts/utils/make-dist-package-json.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ function processExportMap(m) {
}
processExportMap(pkgJson.exports);

if (pkgJson.imports?.['#x509-transport-state']) {
const state = pkgJson.imports['#x509-transport-state'];
for (const condition of Object.keys(state)) {
if (condition === 'types') {
continue;
}
state[condition] = state[condition]
.replace(/^\.\/src\//, './')
.replace(/\.cts$/, '.cjs')
.replace(/\.ts$/, '.mjs');
}
}

for (const key of ['types', 'main', 'module']) {
if (typeof pkgJson[key] === 'string') {
pkgJson[key] = pkgJson[key].replace(/^(\.\/)?dist\//, './');
Expand Down
32 changes: 31 additions & 1 deletion src/auth/x509-transport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
export { createX509Transport } from '../internal/auth/x509-transport-capability';
import { AsyncLocalStorage } from 'node:async_hooks';
import {
createX509Transport as createCapability,
registerX509Transport,
sendX509Request,
} from '../internal/auth/x509-transport-capability';
import type { X509Transport, X509TransportOptions } from '../internal/auth/x509-transport-capability';
import { exchangeX509Token } from '../internal/auth/x509-token-exchange';
import type { X509RequestScope } from '../internal/auth/x509-transport-registry';

/** Creates one frozen, caller-attested Node.js transport for X.509 workload authentication. */
export function createX509Transport(options: X509TransportOptions): X509Transport {
const capability = createCapability(options);
const scopes = new AsyncLocalStorage<X509RequestScope>();
registerX509Transport(capability, {
dispatch: async (target, requestOptions) => await sendX509Request(capability, target, requestOptions),
exchange: async (identityProviderId, serviceAccountId, signal) =>
await exchangeX509Token({
transport: capability,
identityProviderId,
serviceAccountId,
...(signal ? { signal } : {}),
}),
run: (operation) =>
scopes.run({ wallStartedAt: Date.now(), monotonicStartedAt: performance.now() }, operation),
current: () => scopes.getStore(),
resume: (scope, operation) => scopes.run(scope, operation),
});
return capability;
}

export type {
X509ProxyMode,
X509Transport,
Expand Down
24 changes: 9 additions & 15 deletions src/internal/auth/x509-token-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
} from '../../core/error';
import { hasOwn } from '../utils/values';
import { sendX509Request } from './x509-transport-capability';
import type { X509Transport } from './x509-transport-capability';
import type { X509ExchangedToken, X509Transport } from './x509-transport-registry';

export type { X509ExchangedToken } from './x509-transport-registry';

const TOKEN_EXCHANGE_URL = new URL('https://mtls.auth.openai.com/oauth/token');
const TOKEN_EXCHANGE_GRANT = 'urn:ietf:params:oauth:grant-type:token-exchange';
Expand All @@ -33,15 +35,6 @@ export interface X509TokenExchangeOptions {
signal?: AbortSignal | undefined;
}

/** Fully validated, short-lived OAuth credential returned by the X.509 issuer. */
export interface X509ExchangedToken {
/** Header-safe OAuth bearer token; never persisted or included in error messages. */
accessToken: string;

/** Positive issuer-granted lifetime in seconds, never exceeding one hour. */
expiresIn: number;
}

async function cancelReader(
reader: ReadableStreamDefaultReader<Uint8Array>,
reason?: unknown,
Expand Down Expand Up @@ -201,8 +194,9 @@ async function oauthError(response: Response, signal?: AbortSignal): Promise<OAu

/** Exchanges one enrolled client certificate for a validated OpenAI workload access token. */
export async function exchangeX509Token(options: X509TokenExchangeOptions): Promise<X509ExchangedToken> {
options.signal?.throwIfAborted();
const { identityProviderId, serviceAccountId } = options;
const { signal: callerSignal } = options;
callerSignal?.throwIfAborted();
const { identityProviderId, serviceAccountId, transport } = options;
if (
typeof identityProviderId !== 'string' ||
identityProviderId.trim().length === 0 ||
Expand All @@ -215,8 +209,8 @@ export async function exchangeX509Token(options: X509TokenExchangeOptions): Prom
}

const timeoutController = new AbortController();
const signal = options.signal
? AbortSignal.any([options.signal, timeoutController.signal])
const signal = callerSignal
? AbortSignal.any([callerSignal, timeoutController.signal])
: timeoutController.signal;
const timeout = setTimeout(() => {
timeoutController.abort(
Expand All @@ -235,7 +229,7 @@ export async function exchangeX509Token(options: X509TokenExchangeOptions): Prom

let response: Response;
try {
response = await sendX509Request(options.transport, TOKEN_EXCHANGE_URL, {
response = await sendX509Request(transport, TOKEN_EXCHANGE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
Expand Down
32 changes: 24 additions & 8 deletions src/internal/auth/x509-transport-capability.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { types } from 'node:util';
import { Agent, ProxyAgent, Request, fetch } from 'undici';
import { OpenAIError } from '../../core/error';
import { x509TransportBrand } from './x509-transport-registry';
import type { RegisteredX509Transport, X509Transport } from './x509-transport-registry';
import { findRegisteredX509Transport, rememberRegisteredX509Transport } from '#x509-transport-state';

declare const x509TransportBrand: unique symbol;
export type { X509Transport } from './x509-transport-registry';

/** Explicitly supported, application-owned Undici proxy configurations. */
export type X509ProxyMode = 'direct' | 'http-connect' | 'https-connect';

/** An opaque, immutable X.509 transport identity created by {@link createX509Transport}. */
export interface X509Transport {
/** Prevents ordinary objects from being accepted as X.509 transport capabilities. */
readonly [x509TransportBrand]: true;
}

/** Application attestation for one caller-owned static-certificate Undici transport. */
export interface X509TransportOptions {
/** X.509 transport currently supports genuine Node.js runtimes only. */
Expand All @@ -28,9 +26,10 @@ export interface X509TransportOptions {
}

const allowedOptionNames = new Set(['runtime', 'dispatcher', 'certificateIdentity', 'proxy']);
const transportBrand: typeof x509TransportBrand = x509TransportBrand;

class NodeX509Transport implements X509Transport {
declare readonly [x509TransportBrand]: true;
declare readonly [transportBrand]: true;

readonly #dispatcher: Agent | ProxyAgent;

Expand All @@ -44,6 +43,23 @@ class NodeX509Transport implements X509Transport {
}
}

/** Registers only a genuine frozen capability whose JavaScript private dispatcher cannot be forged. */
export function registerX509Transport(transport: X509Transport, registered: RegisteredX509Transport): void {
if (
!transport ||
typeof transport !== 'object' ||
types.isProxy(transport) ||
!Object.isFrozen(transport) ||
!NodeX509Transport.dispatcher(transport)
) {
throw new OpenAIError('Only a genuine frozen X.509 transport capability can be registered.');
}
if (findRegisteredX509Transport(transport)) {
throw new OpenAIError('An approved X.509 transport capability cannot be registered more than once.');
}
rememberRegisteredX509Transport(transport, Object.freeze(registered));
}

function dataOption(options: X509TransportOptions, name: keyof X509TransportOptions): unknown {
const descriptor = Object.getOwnPropertyDescriptor(options, name);
if (!descriptor || !('value' in descriptor)) {
Expand Down
63 changes: 63 additions & 0 deletions src/internal/auth/x509-transport-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { OpenAIError } from '../../core/error';
import { findRegisteredX509Transport } from '#x509-transport-state';

export const x509TransportBrand: unique symbol = Symbol('X.509 transport capability');

/** Opaque identity of one frozen, caller-owned, explicitly attested X.509 transport. */
export interface X509Transport {
/** Prevents ordinary objects from satisfying the transport capability contract. */
readonly [x509TransportBrand]: true;
}

/** Validated short-lived token exchanged using a registered certificate identity. */
export interface X509ExchangedToken {
/** Header-safe, in-memory OpenAI bearer credential. */
accessToken: string;

/** Issuer-approved positive token lifetime in seconds. */
expiresIn: number;
}

/** One logical certificate-authenticated operation and its immutable starting deadline. */
export interface X509RequestScope {
wallStartedAt: number;
monotonicStartedAt: number;
token?: string;
headers?: Headers;
authorization?: string | null;
}

/** Private, peer-independent operations attached to one approved transport generation. */
export interface RegisteredX509Transport {
/** Dispatches one request through the exact frozen certificate capability. */
dispatch: (target: URL, options: RequestInit) => Promise<Response>;

/** Exchanges the same certificate identity for an OpenAI service-account bearer. */
exchange: (
identityProviderId: string,
serviceAccountId: string,
signal?: AbortSignal,
) => Promise<X509ExchangedToken>;

/** Establishes one isolated Node async context without exposing Node dependencies to the root SDK. */
run: <T>(operation: () => T) => T;

/** Resolves only the current logical request's private certificate-authentication scope. */
current: () => X509RequestScope | undefined;

/** Re-enters the original request scope when response parsing resumes outside its promise chain. */
resume: <T>(scope: X509RequestScope, operation: () => T) => T;
}

/** Resolves a previously registered opaque capability without importing an optional transport peer. */
export function resolveX509Transport(value: unknown): RegisteredX509Transport {
if (!value || typeof value !== 'object') {
throw new OpenAIError('X.509 workload identity requires an approved X.509 transport capability.');
}

const registered: RegisteredX509Transport | undefined = findRegisteredX509Transport(value);
if (!registered) {
throw new OpenAIError('X.509 workload identity requires an approved X.509 transport capability.');
}
return registered;
}
15 changes: 15 additions & 0 deletions src/internal/auth/x509-transport-state-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** Browser-safe capability state keeps CommonJS outside the ordinary SDK ESM graph. */
const registeredX509Transports = new WeakMap();
const transientX509ConnectionErrors = new WeakSet();

/** Looks up an opaque capability without exposing the registry itself. */
export const findRegisteredX509Transport = WeakMap.prototype.get.bind(registeredX509Transports);

/** Records a capability only after its Node-only factory validates the private dispatcher. */
export const rememberRegisteredX509Transport = WeakMap.prototype.set.bind(registeredX509Transports);

/** Privately classifies sanitized issuer errors without exposing transport details. */
export const markTransientX509ConnectionError = WeakSet.prototype.add.bind(transientX509ConnectionErrors);

/** Checks the private transient classification without retaining caller-owned errors. */
export const isTransientX509ConnectionError = WeakSet.prototype.has.bind(transientX509ConnectionErrors);
8 changes: 8 additions & 0 deletions src/internal/auth/x509-transport-state.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** One lexical capability registry remains authoritative across mixed module formats. */
const registeredX509Transports = new WeakMap();

/** Looks up an opaque capability without exposing the registry itself. */
export const findRegisteredX509Transport = WeakMap.prototype.get.bind(registeredX509Transports);

/** Records a capability only after the Node-only factory verifies its genuine private dispatcher. */
export const rememberRegisteredX509Transport = WeakMap.prototype.set.bind(registeredX509Transports);
Loading
Loading