Skip to content

Commit 7d3936c

Browse files
authored
Refactor Copilot auth routing to use shared GitHub server host classification (#6400)
* Initial plan * refactor(api-proxy): dedupe GitHub server host classification --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 75dbf9d commit 7d3936c

2 files changed

Lines changed: 52 additions & 36 deletions

File tree

containers/api-proxy/copilot-auth.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {
33
resolveCopilotAuthToken,
44
resolveApiKey,
55
stripBearerPrefix,
6+
classifyGithubServerHost,
67
isGhesInstance,
78
copilotTargetRequiresGitHubTokenPrefix,
89
},
@@ -227,6 +228,28 @@ describe('isGhesInstance', () => {
227228
});
228229
});
229230

231+
describe('classifyGithubServerHost', () => {
232+
it('classifies github.com as github', () => {
233+
expect(classifyGithubServerHost({ GITHUB_SERVER_URL: 'https://github.com' })).toEqual({ kind: 'github' });
234+
});
235+
236+
it('classifies *.ghe.com hosts as ghec and returns the tenant subdomain', () => {
237+
expect(classifyGithubServerHost({ GITHUB_SERVER_URL: 'https://myorg.ghe.com' })).toEqual({
238+
kind: 'ghec',
239+
subdomain: 'myorg',
240+
});
241+
});
242+
243+
it('classifies non-ghe.com enterprise hosts as ghes', () => {
244+
expect(classifyGithubServerHost({ GITHUB_SERVER_URL: 'https://ghes.mycompany.com' })).toEqual({ kind: 'ghes' });
245+
});
246+
247+
it('classifies invalid or missing values safely', () => {
248+
expect(classifyGithubServerHost({ GITHUB_SERVER_URL: 'not-a-url' })).toEqual({ kind: 'invalid' });
249+
expect(classifyGithubServerHost({})).toEqual({ kind: 'missing' });
250+
});
251+
});
252+
230253
describe('copilotTargetRequiresGitHubTokenPrefix', () => {
231254
it('returns true for the Enterprise Copilot endpoint', () => {
232255
expect(copilotTargetRequiresGitHubTokenPrefix('api.enterprise.githubcopilot.com', {})).toBe(true);

containers/api-proxy/providers/copilot-auth.js

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ function resolveCopilotAuthToken(env = process.env) {
7171
return resolveApiKey(env) || stripBearerPrefix(env.COPILOT_GITHUB_TOKEN);
7272
}
7373

74+
/**
75+
* Classify GITHUB_SERVER_URL by host type for auth-routing derivation logic.
76+
*
77+
* @param {Record<string, string|undefined>} env - Environment variables
78+
* @returns {{kind: 'missing'|'invalid'|'github'|'ghec'|'ghes', subdomain?: string}}
79+
*/
80+
function classifyGithubServerHost(env = process.env) {
81+
const serverUrl = env.GITHUB_SERVER_URL;
82+
if (!serverUrl) return { kind: 'missing' };
83+
try {
84+
const hostname = new URL(serverUrl).hostname;
85+
if (hostname === 'github.com') return { kind: 'github' };
86+
if (hostname.endsWith('.ghe.com')) {
87+
return { kind: 'ghec', subdomain: hostname.slice(0, -8) };
88+
}
89+
return { kind: 'ghes' };
90+
} catch {
91+
return { kind: 'invalid' };
92+
}
93+
}
94+
7495
/**
7596
* Derive the Copilot API target hostname from environment variables.
7697
*
@@ -91,21 +112,9 @@ function deriveCopilotApiTarget(env = process.env) {
91112
// fall through to auto-derivation when the value is malformed.
92113
if (target) return target;
93114
}
94-
const serverUrl = env.GITHUB_SERVER_URL;
95-
if (serverUrl) {
96-
try {
97-
const hostname = new URL(serverUrl).hostname;
98-
if (hostname !== 'github.com') {
99-
if (hostname.endsWith('.ghe.com')) {
100-
const subdomain = hostname.slice(0, -8); // Remove '.ghe.com'
101-
return `copilot-api.${subdomain}.ghe.com`;
102-
}
103-
return 'api.enterprise.githubcopilot.com';
104-
}
105-
} catch {
106-
// Invalid URL — fall through to default
107-
}
108-
}
115+
const serverHost = classifyGithubServerHost(env);
116+
if (serverHost.kind === 'ghec') return `copilot-api.${serverHost.subdomain}.ghe.com`;
117+
if (serverHost.kind === 'ghes') return 'api.enterprise.githubcopilot.com';
109118
return 'api.githubcopilot.com';
110119
}
111120

@@ -125,18 +134,8 @@ function deriveGitHubApiTarget(env = process.env) {
125134
const target = normalizeApiTarget(env.GITHUB_API_URL);
126135
if (target) return target;
127136
}
128-
const serverUrl = env.GITHUB_SERVER_URL;
129-
if (serverUrl) {
130-
try {
131-
const hostname = new URL(serverUrl).hostname;
132-
if (hostname !== 'github.com' && hostname.endsWith('.ghe.com')) {
133-
const subdomain = hostname.slice(0, -8);
134-
return `api.${subdomain}.ghe.com`;
135-
}
136-
} catch {
137-
// Invalid URL — fall through to default
138-
}
139-
}
137+
const serverHost = classifyGithubServerHost(env);
138+
if (serverHost.kind === 'ghec') return `api.${serverHost.subdomain}.ghe.com`;
140139
return 'api.github.com';
141140
}
142141

@@ -228,15 +227,7 @@ function isGhesInstance(resolvedTarget, env = process.env) {
228227
if (env.AWF_PLATFORM_TYPE && env.AWF_PLATFORM_TYPE !== 'ghes') return false;
229228

230229
if (resolvedTarget === 'api.enterprise.githubcopilot.com') return true;
231-
232-
const serverUrl = env.GITHUB_SERVER_URL;
233-
if (!serverUrl) return false;
234-
try {
235-
const hostname = new URL(serverUrl).hostname;
236-
return hostname !== 'github.com' && !hostname.endsWith('.ghe.com');
237-
} catch {
238-
return false;
239-
}
230+
return classifyGithubServerHost(env).kind === 'ghes';
240231
}
241232

242233
/**
@@ -303,6 +294,7 @@ module.exports = {
303294
deriveGitHubApiBasePath,
304295
isGithubCopilotCatalogTarget,
305296
isGhesInstance,
297+
classifyGithubServerHost,
306298
copilotTargetRequiresGitHubTokenPrefix,
307299
getCopilotModelFallbackPolicy,
308300
// Exported for unit-test access only; not part of the public API.
@@ -315,6 +307,7 @@ module.exports = {
315307
deriveGitHubApiBasePath,
316308
isGithubCopilotCatalogTarget,
317309
isGhesInstance,
310+
classifyGithubServerHost,
318311
copilotTargetRequiresGitHubTokenPrefix,
319312
getCopilotModelFallbackPolicy,
320313
},

0 commit comments

Comments
 (0)