From 941fe2c1e31c00a86ddb090055957426918cbd7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:49:54 +0000 Subject: [PATCH 1/2] Initial plan From 018d800e51f32a5a71da3c90733b2c6224f12981 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:55:27 +0000 Subject: [PATCH 2/2] refactor: extract shared tokenAuthHeaders helper for copilot auth-prefix logic Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../api-proxy/providers/auth-headers.js | 22 ++++++++++++++-- .../api-proxy/providers/auth-headers.test.js | 26 ++++++++++++++++++- containers/api-proxy/providers/copilot.js | 25 +++++++++--------- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/containers/api-proxy/providers/auth-headers.js b/containers/api-proxy/providers/auth-headers.js index 4b0e03317..274959c85 100644 --- a/containers/api-proxy/providers/auth-headers.js +++ b/containers/api-proxy/providers/auth-headers.js @@ -8,6 +8,24 @@ * integration-metadata key only requires a single edit. */ +/** + * Build an `Authorization` header using the given scheme prefix (e.g. `Bearer`, + * `token`), optionally merged with extra headers. + * + * This is the single place that assembles an `Authorization` header value, so + * every adapter that needs a non-default prefix (e.g. Copilot's Enterprise + * `token ` scheme) composes it from here instead of concatenating the + * prefix and token itself. + * + * @param {string} prefix - The auth scheme prefix (e.g. `Bearer`, `token`) + * @param {string} token + * @param {Record} [extraHeaders] + * @returns {Record} + */ +function tokenAuthHeaders(prefix, token, extraHeaders) { + return { ...extraHeaders, 'Authorization': prefix + ' ' + token }; +} + /** * Build a `Bearer` Authorization header, optionally merged with extra headers. * @@ -16,7 +34,7 @@ * @returns {Record} */ function bearerAuthHeaders(token, extraHeaders) { - return { ...extraHeaders, 'Authorization': 'Bearer ' + token }; + return tokenAuthHeaders('Bearer', token, extraHeaders); } /** @@ -43,4 +61,4 @@ function withCopilotIntegration(headers, integrationId) { return { ...headers, 'Copilot-Integration-Id': integrationId }; } -module.exports = { bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration }; +module.exports = { tokenAuthHeaders, bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration }; diff --git a/containers/api-proxy/providers/auth-headers.test.js b/containers/api-proxy/providers/auth-headers.test.js index b0c29632c..ad226a881 100644 --- a/containers/api-proxy/providers/auth-headers.test.js +++ b/containers/api-proxy/providers/auth-headers.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration } = require('./auth-headers'); +const { tokenAuthHeaders, bearerAuthHeaders, providerKeyHeaders, withCopilotIntegration } = require('./auth-headers'); describe('bearerAuthHeaders', () => { it('builds an Authorization: Bearer ... header', () => { @@ -68,3 +68,27 @@ describe('withCopilotIntegration', () => { }); }); }); + +describe('tokenAuthHeaders', () => { + it('builds an Authorization header using the given prefix and token', () => { + expect(tokenAuthHeaders('token', 'gh-tok')).toEqual({ 'Authorization': 'token gh-tok' }); + }); + + + it('merges extra headers alongside the Authorization header', () => { + expect(tokenAuthHeaders('token', 'gh-tok', { 'X-GitHub-Api-Version': '2026-07-01' })).toEqual({ + 'X-GitHub-Api-Version': '2026-07-01', + 'Authorization': 'token gh-tok', + }); + }); + + it('does not mutate the extraHeaders argument', () => { + const extra = { 'x-custom': 'val' }; + tokenAuthHeaders('token', 'gh-tok', extra); + expect(extra).toEqual({ 'x-custom': 'val' }); + }); + + it('is the basis for bearerAuthHeaders', () => { + expect(bearerAuthHeaders('tok')).toEqual(tokenAuthHeaders('Bearer', 'tok')); + }); +}); diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index 1eaba8f53..62223e74a 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -38,7 +38,7 @@ const { copilotTargetRequiresGitHubTokenPrefix, isGithubCopilotCatalogTarget, } = require('./copilot-auth'); -const { bearerAuthHeaders, withCopilotIntegration } = require('./auth-headers'); +const { bearerAuthHeaders, tokenAuthHeaders, withCopilotIntegration } = require('./auth-headers'); const { URL } = require('url'); const { COPILOT_ENV } = require('../provider-env-constants'); @@ -124,6 +124,9 @@ function createCopilotAdapter(env, deps = {}) { : null; const bodyTransform = composeBodyTransforms(sanitizedBodyTransform, byokBodyFieldTransform); const requiresGitHubTokenPrefix = copilotTargetRequiresGitHubTokenPrefix(rawTarget, env); + // The GitHub OAuth token always uses this prefix (Enterprise/Business targets + // require 'token', everything else 'Bearer'); BYOK API keys always use 'Bearer'. + const githubTokenAuthPrefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer'; const authPrefix = (requiresGitHubTokenPrefix && !apiKey) ? 'token' : 'Bearer'; // Pre-computed models path used by getModelsFetchConfig and getReflectionInfo. // For BYOK/custom providers the base path prefix is included (e.g. /api/v1/models @@ -142,15 +145,14 @@ function createCopilotAdapter(env, deps = {}) { * @returns {{ url: string, opts: { method: string, headers: Record } } & Record} */ function buildCopilotModelsRequest(extra = {}) { - const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer'; return { url: `https://${rawTarget}/models`, opts: { method: 'GET', - headers: withCopilotIntegration({ - 'Authorization': prefix + ' ' + githubToken, - 'X-GitHub-Api-Version': COPILOT_MODELS_API_VERSION, - }, integrationId), + headers: withCopilotIntegration( + tokenAuthHeaders(githubTokenAuthPrefix, githubToken, { 'X-GitHub-Api-Version': COPILOT_MODELS_API_VERSION }), + integrationId + ), }, modelMetadataFormat: 'copilot', apiVersion: COPILOT_MODELS_API_VERSION, @@ -165,10 +167,10 @@ function createCopilotAdapter(env, deps = {}) { env, oidcAuthOptions: { staticAuthToken: authToken, skipWhen: !!staticAuthToken }, buildOidcHeaders: (token) => withCopilotIntegration(bearerAuthHeaders(token), integrationId), - buildStaticHeaders: () => withCopilotIntegration({ - ...(apiKey ? byokExtraHeaders : {}), - 'Authorization': authPrefix + ' ' + authToken, - }, integrationId), + buildStaticHeaders: () => withCopilotIntegration( + tokenAuthHeaders(authPrefix, authToken, apiKey ? byokExtraHeaders : undefined), + integrationId + ), createAdapterMethodsOptions: ({ oidcConfigured, authProvider }) => ({ apiKey: authToken, rawTarget, @@ -282,8 +284,7 @@ function createCopilotAdapter(env, deps = {}) { const isModelsPath = reqPathname === '/models' || reqPathname.startsWith('/models/'); if (isModelsPath && req.method === 'GET' && githubToken) { // /models always uses the GitHub OAuth token (not BYOK key) - const prefix = requiresGitHubTokenPrefix ? 'token' : 'Bearer'; - return withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId); + return withCopilotIntegration(tokenAuthHeaders(githubTokenAuthPrefix, githubToken), integrationId); } const headers = resolveHeaders();