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
31 changes: 31 additions & 0 deletions containers/api-proxy/adapter-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,36 @@ function createAdapterMethods(opts) {
};
}

/**
* Bundle the repeated env-reading and deps-extraction boilerplate common to
* all non-Copilot provider adapters:
*
* createBaseAdapterConfig(env, envVars) → { apiKey, rawTarget, basePath }
* deps.bodyTransform ?? null → bodyTransform
*
* Centralising this pattern reduces the number of places where
* security-sensitive credential env-var names are referenced directly and
* makes it easier to add cross-cutting concerns (e.g. audit logging,
* key-validation hooks) in one place rather than in every provider file.
*
* @param {Record<string, string|undefined>} env - Environment variables
* @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise<Buffer | null>)) | null }} [deps={}] - Injected dependencies
* @param {object} envVars
* @param {string} envVars.keyEnvVar - e.g. 'GEMINI_API_KEY'
* @param {string} envVars.targetEnvVar - e.g. 'GEMINI_API_TARGET'
* @param {string} envVars.basePathEnvVar - e.g. 'GEMINI_API_BASE_PATH'
* @param {string} envVars.defaultTarget - e.g. 'generativelanguage.googleapis.com'
* @returns {{ apiKey: string|undefined, rawTarget: string, basePath: string,
* bodyTransform: ((body: Buffer) => (Buffer | null | Promise<Buffer | null>)) | null }}
*/
function createProviderAuthScaffold(env, deps = {}, { keyEnvVar, targetEnvVar, basePathEnvVar, defaultTarget }) {
const { apiKey, rawTarget, basePath } = createBaseAdapterConfig(env, {
keyEnvVar, targetEnvVar, basePathEnvVar, defaultTarget,
});
const bodyTransform = (deps != null && deps.bodyTransform != null) ? deps.bodyTransform : null;
return { apiKey, rawTarget, basePath, bodyTransform };
}

/**
* Assemble a provider adapter object from its constituent parts.
*
Expand Down Expand Up @@ -216,6 +246,7 @@ function buildProviderAdapter({

module.exports = {
createBaseAdapterConfig,
createProviderAuthScaffold,
createAdapterMethods,
buildProviderAdapter,
};
59 changes: 59 additions & 0 deletions containers/api-proxy/adapter-factory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,62 @@ describe('buildProviderAdapter', () => {
});
});
});

describe('createProviderAuthScaffold', () => {
const { createProviderAuthScaffold } = require('./adapter-factory');
const envVars = {
keyEnvVar: 'MY_API_KEY',
targetEnvVar: 'MY_API_TARGET',
basePathEnvVar: 'MY_API_BASE_PATH',
defaultTarget: 'api.example.com',
};

it('reads apiKey, rawTarget, basePath from env using the given env-var names', () => {
const env = { MY_API_KEY: 'secret-key', MY_API_TARGET: 'custom.example.com', MY_API_BASE_PATH: '/v2' };
const result = createProviderAuthScaffold(env, {}, envVars);
expect(result.apiKey).toBe('secret-key');
expect(result.rawTarget).toBe('custom.example.com');
expect(result.basePath).toBe('/v2');
});

it('returns undefined apiKey when the key env var is absent', () => {
const result = createProviderAuthScaffold({}, {}, envVars);
expect(result.apiKey).toBeUndefined();
});

it('falls back to defaultTarget when the target env var is absent', () => {
const result = createProviderAuthScaffold({}, {}, envVars);
expect(result.rawTarget).toBe('api.example.com');
});

it('returns the deps bodyTransform when provided', () => {
const transform = (body) => body;
const result = createProviderAuthScaffold({}, { bodyTransform: transform }, envVars);
expect(result.bodyTransform).toBe(transform);
});

it('returns null bodyTransform when deps is empty', () => {
const result = createProviderAuthScaffold({}, {}, envVars);
expect(result.bodyTransform).toBeNull();
});

it('returns null bodyTransform when deps.bodyTransform is null', () => {
const result = createProviderAuthScaffold({}, { bodyTransform: null }, envVars);
expect(result.bodyTransform).toBeNull();
});

it('returns null bodyTransform when deps is omitted', () => {
const result = createProviderAuthScaffold({}, undefined, envVars);
expect(result.bodyTransform).toBeNull();
});

it('trims whitespace from apiKey and treats blank string as undefined', () => {
const result = createProviderAuthScaffold({ MY_API_KEY: ' ' }, {}, envVars);
expect(result.apiKey).toBeUndefined();
});

it('strips https:// protocol prefix from rawTarget', () => {
const result = createProviderAuthScaffold({ MY_API_TARGET: 'https://custom.example.com' }, {}, envVars);
expect(result.rawTarget).toBe('custom.example.com');
});
});
8 changes: 3 additions & 5 deletions containers/api-proxy/providers/anthropic.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const {
resolveOidcAuthHeaders,
resolveAuthHeadersWithFallback,
} = require('../oidc-adapter-utils');
const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { AnthropicOidcTokenProvider } = require('../anthropic-oidc-token-provider');
const { ANTHROPIC_ENV } = require('../provider-env-constants');
const { createProviderOidcAuth } = require('./cloud-oidc-init');
Expand All @@ -48,7 +48,7 @@ try {
* @returns {import('./index').ProviderAdapter}
*/
function createAnthropicAdapter(env, deps = {}) {
const { apiKey, rawTarget, basePath } = createBaseAdapterConfig(env, {
const { apiKey, rawTarget, basePath, bodyTransform: depsBodyTransform } = createProviderAuthScaffold(env, deps, {
keyEnvVar: ANTHROPIC_ENV.KEY,
targetEnvVar: ANTHROPIC_ENV.TARGET,
basePathEnvVar: ANTHROPIC_ENV.BASE_PATH,
Expand Down Expand Up @@ -113,11 +113,9 @@ function createAnthropicAdapter(env, deps = {}) {
customTransform,
});

const bodyTransform = deps.bodyTransform || null;

// Build the composed transform once at construction time to avoid
// re-allocating the wrapper function on every request.
const composedBodyTransform = composeBodyTransforms(bodyTransform, optimisationsTransform);
const composedBodyTransform = composeBodyTransforms(depsBodyTransform, optimisationsTransform);
const adapterMethods = createAdapterMethods({
apiKey,
rawTarget,
Expand Down
5 changes: 2 additions & 3 deletions containers/api-proxy/providers/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

const { stripGeminiKeyParam, makeUnconfiguredHealthResponse } = require('../proxy-utils');
const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { GEMINI_ENV } = require('../provider-env-constants');

/**
Expand All @@ -25,14 +25,13 @@ const { GEMINI_ENV } = require('../provider-env-constants');
* @returns {import('./index').ProviderAdapter}
*/
function createGeminiAdapter(env, deps = {}) {
const { apiKey, rawTarget, basePath } = createBaseAdapterConfig(env, {
const { apiKey, rawTarget, basePath, bodyTransform } = createProviderAuthScaffold(env, deps, {
keyEnvVar: GEMINI_ENV.KEY,
targetEnvVar: GEMINI_ENV.TARGET,
basePathEnvVar: GEMINI_ENV.BASE_PATH,
defaultTarget: 'generativelanguage.googleapis.com',
});

const bodyTransform = deps.bodyTransform || null;
const adapterMethods = createAdapterMethods({
apiKey,
rawTarget,
Expand Down
11 changes: 7 additions & 4 deletions containers/api-proxy/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
} = require('../proxy-utils');
const { validateAuthHeaderEnv } = require('../oidc-adapter-utils');

const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory');
const { createProviderOidcAuth } = require('./cloud-oidc-init');
const { OPENAI_ENV, COPILOT_ENV } = require('../provider-env-constants');

Expand All @@ -28,7 +28,12 @@ const { OPENAI_ENV, COPILOT_ENV } = require('../provider-env-constants');
* @returns {import('./index').ProviderAdapter}
*/
function createOpenAIAdapter(env, deps = {}) {
const { apiKey: openaiApiKey, rawTarget: openaiTarget, basePath: openaiBasePath } = createBaseAdapterConfig(env, {
const {
apiKey: openaiApiKey,
rawTarget: openaiTarget,
basePath: openaiBasePath,
bodyTransform,
} = createProviderAuthScaffold(env, deps, {
keyEnvVar: OPENAI_ENV.KEY,
targetEnvVar: OPENAI_ENV.TARGET,
basePathEnvVar: OPENAI_ENV.BASE_PATH,
Expand Down Expand Up @@ -57,8 +62,6 @@ function createOpenAIAdapter(env, deps = {}) {
// Custom targets manage their own path layout and must not receive an implicit prefix.
const basePath = explicitBasePath || (rawTarget === 'api.openai.com' ? '/v1' : '');

const bodyTransform = deps.bodyTransform || null;

// OIDC auth strategy (Azure OpenAI, AWS Bedrock, GCP Vertex AI)
const {
authProvider, oidcConfigured,
Expand Down
Loading