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
25 changes: 22 additions & 3 deletions containers/api-proxy/copilot-adapter-enterprise.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const { createCopilotAdapter } = require('./providers/copilot');

const bearerByokKey = ['Bearer', 'sk-byok-key'].join(' ');
const bearerStandardToken = ['Bearer', 'ghu_standard_token_123'].join(' ');
const bearerGhecToken = ['Bearer', 'ghu_ghec_token_123'].join(' ');
const bearerCustomToken = ['Bearer', 'ghu_standard_token_123'].join(' ');
const bearerGithubComOverrideToken = ['Bearer', 'ghu_token_123'].join(' ');
const tokenGhecToken = ['token', 'ghu_ghec_token_123'].join(' ');

describe('createCopilotAdapter — GHE enterprise auth format', () => {
const fakeReq = { url: '/v1/chat/completions', method: 'POST', headers: {} };
Expand Down Expand Up @@ -56,13 +56,32 @@ describe('createCopilotAdapter — GHE enterprise auth format', () => {
expect(headers['Authorization']).toBe(bearerStandardToken);
});

it('uses "Bearer" prefix for GHEC tenant (*.ghe.com)', () => {
it('uses "token" prefix for a derived GHEC data-residency target', () => {
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_ghec_token_123',
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
});
const headers = adapter.getAuthHeaders(fakeReq);
expect(headers['Authorization']).toBe(bearerGhecToken);
expect(headers['Authorization']).toBe(tokenGhecToken);
});
Comment on lines +59 to +66

it('uses "token" prefix for /models on a derived GHEC data-residency target', () => {
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_ghec_token_123',
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
});
const headers = adapter.getAuthHeaders(fakeModelsReq);
expect(headers['Authorization']).toBe(tokenGhecToken);
});

it('uses "token" prefix for a GHEC target when AWF_PLATFORM_TYPE=ghec', () => {
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_ghec_token_123',
AWF_PLATFORM_TYPE: 'ghec',
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
});
const headers = adapter.getAuthHeaders(fakeReq);
expect(headers['Authorization']).toBe(tokenGhecToken);
});

it('strips "token " prefix from COPILOT_GITHUB_TOKEN before re-prefixing for GHES', () => {
Expand Down
18 changes: 16 additions & 2 deletions containers/api-proxy/copilot-auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,24 @@ describe('copilotTargetRequiresGitHubTokenPrefix', () => {
expect(copilotTargetRequiresGitHubTokenPrefix('api.githubcopilot.com', {})).toBe(false);
});

it('returns false for a *.ghe.com (GHEC) Copilot target', () => {
it('returns true for a GHEC data-residency Copilot target', () => {
expect(copilotTargetRequiresGitHubTokenPrefix('copilot-api.myorg.ghe.com', {
GITHUB_SERVER_URL: 'https://myorg.ghe.com',
})).toBe(false);
})).toBe(true);
});

it('returns true for a GHEC data-residency Copilot target with AWF_PLATFORM_TYPE=ghec', () => {
expect(copilotTargetRequiresGitHubTokenPrefix('copilot-api.myorg.ghe.com', {
AWF_PLATFORM_TYPE: 'ghec',
GITHUB_SERVER_URL: 'https://myorg.ghe.com',
})).toBe(true);
});
Comment on lines +312 to 323

it('returns false for malformed or non-canonical GHEC data-residency target shapes', () => {
const env = { GITHUB_SERVER_URL: 'https://myorg.ghe.com' };
expect(copilotTargetRequiresGitHubTokenPrefix('copilot-api..ghe.com', env)).toBe(false);
expect(copilotTargetRequiresGitHubTokenPrefix('copilot-api.a.b.ghe.com', env)).toBe(false);
expect(copilotTargetRequiresGitHubTokenPrefix('copilot-api.myorg.ghe.com.evil.com', env)).toBe(false);
});

it('returns false when no token-prefix indicators are present', () => {
Expand Down
28 changes: 16 additions & 12 deletions containers/api-proxy/providers/copilot-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,27 +235,29 @@ function isGhesInstance(resolvedTarget, env = process.env) {
* directly and therefore require the `token <value>` Authorization prefix
* (rather than `Bearer <value>`) for GitHub credentials.
*
* Both the Enterprise and Business endpoints behave this way; the standard
* `api.githubcopilot.com` endpoint instead expects a Copilot token with the
* `Bearer` prefix.
* Enterprise, Business, and GHEC data-residency endpoints behave this way; the
* standard `api.githubcopilot.com` endpoint instead expects a Copilot token
* with the `Bearer` prefix.
*/
const GITHUB_TOKEN_PREFIX_COPILOT_TARGETS = new Set([
'api.enterprise.githubcopilot.com',
'api.business.githubcopilot.com',
]);

function isGhecCopilotApiTarget(target) {
return /^copilot-api\.[^.]+\.ghe\.com$/.test(target);
}
Comment on lines +247 to +249

/**
* Decide whether a GitHub OAuth/PAT token sent to the Copilot API must use the
* `token <value>` Authorization prefix instead of `Bearer <value>`.
*
* This is true when either:
* 1. The resolved target is a known GitHub-hosted Copilot endpoint that
* authenticates the GitHub token directly — i.e. the Enterprise or Business
* host. This check takes highest priority and is NOT overridable by
* AWF_PLATFORM_TYPE. Without this ordering, gh-aw's automatic
* AWF_PLATFORM_TYPE=ghec injection on *.ghe.com runners would suppress the
* `token` prefix for Copilot Business customers who set
* COPILOT_API_TARGET=api.business.githubcopilot.com.
* authenticates the GitHub token directly — i.e. the Enterprise, Business,
* or GHEC data-residency host. This check takes highest priority and is NOT
* overridable by AWF_PLATFORM_TYPE. Without this ordering, an explicit
* AWF_PLATFORM_TYPE=ghec would suppress the required `token` prefix.
* 2. The environment is a GHES instance (see {@link isGhesInstance}).
*
* An explicit non-GHES AWF_PLATFORM_TYPE overrides the GHES heuristics (case 2)
Expand All @@ -272,10 +274,12 @@ function copilotTargetRequiresGitHubTokenPrefix(resolvedTarget, env = process.en
// Known GitHub-hosted Copilot endpoints always require the 'token' prefix for
// GitHub OAuth/PAT credentials, regardless of platform type. This check must
// come before the AWF_PLATFORM_TYPE guard so that an explicit platform type
// (e.g. AWF_PLATFORM_TYPE=ghec set by gh-aw on *.ghe.com runners) does not
// suppress the required 'token' prefix for Business/Enterprise endpoints.
// does not suppress the required 'token' prefix.
const target = normalizeApiTarget(resolvedTarget);
if (target && GITHUB_TOKEN_PREFIX_COPILOT_TARGETS.has(target)) return true;
if (target && (
GITHUB_TOKEN_PREFIX_COPILOT_TARGETS.has(target)
|| isGhecCopilotApiTarget(target)
)) return true;

// An explicit non-GHES platform type overrides the GHES heuristics below
// for custom/unknown targets but never overrides catalog endpoints (above).
Expand Down
4 changes: 2 additions & 2 deletions containers/api-proxy/server.auth-matrix.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ describe('Auth Matrix — Copilot', () => {
});

describe('GitHub OAuth token — GHEC (*.ghe.com)', () => {
it('sends Authorization: Bearer (not token) for GHEC', () => {
it('sends Authorization: token for GHEC data-residency targets', () => {
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_ghec_token',
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
});
const headers = adapter.getAuthHeaders(fakeReq());
expect(headers.Authorization).toBe('Bearer ghu_ghec_token');
expect(headers.Authorization).toBe('token ghu_ghec_token');
});

it('derives correct copilot-api target for GHEC', () => {
Expand Down
10 changes: 5 additions & 5 deletions docs-site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"diff": "8.0.3",
"esbuild": "^0.28.1",
"ajv": "8.16.0",
"nanoid": "5.1.16",
"postcss": "8.5.23",
"svgo": "^4.0.2",
"vite": "8.0.16",
Expand Down
Loading