From 468b194e502b7f9a8adf0b1d6f2170ff7bf83f4d Mon Sep 17 00:00:00 2001 From: David Raehles Date: Wed, 18 Feb 2026 20:38:59 +0100 Subject: [PATCH] fix(opencode): exchange OAuth token for Copilot JWT to fix GitHub Enterprise auth --- packages/opencode/src/plugin/copilot.ts | 52 ++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 39ea0d00d28e..4cc4946e7e5a 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -6,6 +6,9 @@ const CLIENT_ID = "Ov23li8tweQw6odWQebz" // Add a small safety buffer when polling to avoid hitting the server // slightly too early due to clock skew / timer drift. const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 // 3 seconds +// Buffer in seconds before JWT expiration to trigger a refresh +const JWT_REFRESH_BUFFER_SECONDS = 120 + function normalizeDomain(url: string) { return url.replace(/^https?:\/\//, "").replace(/\/$/, "") } @@ -17,6 +20,46 @@ function getUrls(domain: string) { } } +// Cache for Copilot JWTs exchanged from OAuth tokens, keyed by domain +const jwtCache = new Map() + +async function exchangeForCopilotJWT( + domain: string, + oauthToken: string, +): Promise { + const cached = jwtCache.get(domain) + if (cached && cached.expiresAt > Date.now() / 1000 + JWT_REFRESH_BUFFER_SECONDS) + return cached.token + + const apiHost = + domain === "github.com" ? "api.github.com" : `api.${domain}` + const response = await fetch( + `https://${apiHost}/copilot_internal/v2/token`, + { + headers: { + Authorization: `token ${oauthToken}`, + Accept: "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + "Editor-Version": `opencode/${Installation.VERSION}`, + }, + }, + ) + + if (!response.ok) return oauthToken + + const data = (await response.json()) as { + token: string + expires_at: number + } + + jwtCache.set(domain, { + token: data.token, + expiresAt: data.expires_at, + }) + + return data.token +} + export async function CopilotAuthPlugin(input: PluginInput): Promise { const sdk = input.client return { @@ -118,11 +161,18 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { return { isVision: false, isAgent: false } }) + const domain = enterpriseUrl + ? normalizeDomain(enterpriseUrl) + : "github.com" + const token = await exchangeForCopilotJWT(domain, info.refresh) + const headers: Record = { "x-initiator": isAgent ? "agent" : "user", ...(init?.headers as Record), "User-Agent": `opencode/${Installation.VERSION}`, - Authorization: `Bearer ${info.refresh}`, + Authorization: `Bearer ${token}`, + "Editor-Version": `opencode/${Installation.VERSION}`, + "Copilot-Integration-Id": "opencode-chat", "Openai-Intent": "conversation-edits", }