diff --git a/containers/api-proxy/adapter-factory.js b/containers/api-proxy/adapter-factory.js index 519034109..34fcc8f9d 100644 --- a/containers/api-proxy/adapter-factory.js +++ b/containers/api-proxy/adapter-factory.js @@ -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} env - Environment variables + * @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise)) | 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)) | 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. * @@ -216,6 +246,7 @@ function buildProviderAdapter({ module.exports = { createBaseAdapterConfig, + createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter, }; diff --git a/containers/api-proxy/adapter-factory.test.js b/containers/api-proxy/adapter-factory.test.js index 795e28345..10e54941e 100644 --- a/containers/api-proxy/adapter-factory.test.js +++ b/containers/api-proxy/adapter-factory.test.js @@ -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'); + }); +}); diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 539b6f93c..221c1be8f 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -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'); @@ -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, @@ -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, diff --git a/containers/api-proxy/providers/gemini.js b/containers/api-proxy/providers/gemini.js index a335f93a6..a5fbc409c 100644 --- a/containers/api-proxy/providers/gemini.js +++ b/containers/api-proxy/providers/gemini.js @@ -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'); /** @@ -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, diff --git a/containers/api-proxy/providers/openai.js b/containers/api-proxy/providers/openai.js index 0f1db9039..08e49cccb 100644 --- a/containers/api-proxy/providers/openai.js +++ b/containers/api-proxy/providers/openai.js @@ -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'); @@ -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, @@ -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,