diff --git a/containers/api-proxy/copilot-adapter-enterprise.test.js b/containers/api-proxy/copilot-adapter-enterprise.test.js index 79be4fe8a..830ff46f0 100644 --- a/containers/api-proxy/copilot-adapter-enterprise.test.js +++ b/containers/api-proxy/copilot-adapter-enterprise.test.js @@ -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: {} }; @@ -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); + }); + + 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', () => { diff --git a/containers/api-proxy/copilot-auth.test.js b/containers/api-proxy/copilot-auth.test.js index a5838ea87..fdbf040ef 100644 --- a/containers/api-proxy/copilot-auth.test.js +++ b/containers/api-proxy/copilot-auth.test.js @@ -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); + }); + + 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', () => { diff --git a/containers/api-proxy/providers/copilot-auth.js b/containers/api-proxy/providers/copilot-auth.js index b662990d6..7557520cc 100644 --- a/containers/api-proxy/providers/copilot-auth.js +++ b/containers/api-proxy/providers/copilot-auth.js @@ -235,27 +235,29 @@ function isGhesInstance(resolvedTarget, env = process.env) { * directly and therefore require the `token ` Authorization prefix * (rather than `Bearer `) 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); +} + /** * Decide whether a GitHub OAuth/PAT token sent to the Copilot API must use the * `token ` Authorization prefix instead of `Bearer `. * * 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) @@ -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). diff --git a/containers/api-proxy/server.auth-matrix.test.js b/containers/api-proxy/server.auth-matrix.test.js index c47098fe6..62ad08d01 100644 --- a/containers/api-proxy/server.auth-matrix.test.js +++ b/containers/api-proxy/server.auth-matrix.test.js @@ -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', () => { diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 9f3e0c4b3..d6ffb4688 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -6651,9 +6651,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "5.1.16", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha1-/jRcCh+QB8Mvu1wTnhIIv9P0Hvc=", "funding": [ { "type": "github", @@ -6662,10 +6662,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/neotraverse": { diff --git a/docs-site/package.json b/docs-site/package.json index b2be16d3d..fc121e770 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -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",