1- /**
2- * @type {import('@opencode-ai/plugin').Plugin }
3- */
4- export async function CopilotAuthPlugin ( { client } ) {
1+ function createCopilotPlugin ( client , { providerId, isEnterprise } ) {
52 const CLIENT_ID = "Iv1.b507a08c87ecfe98" ;
63 const HEADERS = {
74 "User-Agent" : "GitHubCopilotChat/0.35.0" ,
@@ -10,53 +7,23 @@ export async function CopilotAuthPlugin({ client }) {
107 "Copilot-Integration-Id" : "vscode-chat" ,
118 } ;
129
13- /**
14- * Normalizes a domain by removing protocol and trailing slashes
15- */
1610 function normalizeDomain ( url ) {
1711 return url
1812 . replace ( / ^ h t t p s ? : \/ \/ / , "" )
1913 . replace ( / \/ $ / , "" ) ;
2014 }
2115
22- /**
23- * Gets the base URL from auth data, provider config, or defaults to github.com
24- * Priority: auth data > config > github.com
25- */
26- async function getBaseUrl ( providerId , authInfo ) {
27- try {
28- // First check auth data (set by core during authentication)
29- if ( authInfo && authInfo . enterpriseUrl ) {
30- return normalizeDomain ( authInfo . enterpriseUrl ) ;
31- }
32-
33- // Then check config
34- const config = await client . config . get ( ) ;
35- const providerConfig = config ?. provider ?. [ providerId ] ;
36- const configUrl = providerConfig ?. options ?. enterpriseUrl ;
37-
38- return configUrl ? normalizeDomain ( configUrl ) : "github.com" ;
39- } catch {
40- return "github.com" ;
41- }
42- }
43-
44- /**
45- * Constructs URLs based on the base URL (github.com or enterprise)
46- */
47- async function getUrls ( providerId , authInfo ) {
48- const baseUrl = await getBaseUrl ( providerId , authInfo ) ;
49-
16+ function getUrls ( domain ) {
5017 return {
51- DEVICE_CODE_URL : `https://${ baseUrl } /login/device/code` ,
52- ACCESS_TOKEN_URL : `https://${ baseUrl } /login/oauth/access_token` ,
53- COPILOT_API_KEY_URL : `https://api.${ baseUrl } /copilot_internal/v2/token` ,
18+ DEVICE_CODE_URL : `https://${ domain } /login/device/code` ,
19+ ACCESS_TOKEN_URL : `https://${ domain } /login/oauth/access_token` ,
20+ COPILOT_API_KEY_URL : `https://api.${ domain } /copilot_internal/v2/token` ,
5421 } ;
5522 }
5623
5724 return {
5825 auth : {
59- provider : "github-copilot" ,
26+ provider : providerId ,
6027 loader : async ( getAuth , provider ) => {
6128 let info = await getAuth ( ) ;
6229 if ( ! info || info . type !== "oauth" ) return { } ;
@@ -70,13 +37,24 @@ export async function CopilotAuthPlugin({ client }) {
7037 }
7138 }
7239
40+ // For enterprise, set baseURL dynamically based on enterpriseUrl in auth data
41+ const enterpriseUrl = info . enterpriseUrl ;
42+ const baseURL = enterpriseUrl
43+ ? `https://copilot-api.${ normalizeDomain ( enterpriseUrl ) } `
44+ : undefined ;
45+
7346 return {
47+ ...( baseURL && { baseURL } ) ,
7448 apiKey : "" ,
7549 async fetch ( input , init ) {
7650 const info = await getAuth ( ) ;
7751 if ( info . type !== "oauth" ) return { } ;
7852 if ( ! info . access || info . expires < Date . now ( ) ) {
79- const urls = await getUrls ( provider . id , info ) ;
53+ const domain = info . enterpriseUrl
54+ ? normalizeDomain ( info . enterpriseUrl )
55+ : "github.com" ;
56+ const urls = getUrls ( domain ) ;
57+
8058 const response = await fetch ( urls . COPILOT_API_KEY_URL , {
8159 headers : {
8260 Accept : "application/json" ,
@@ -98,6 +76,7 @@ export async function CopilotAuthPlugin({ client }) {
9876 refresh : info . refresh ,
9977 access : tokenData . token ,
10078 expires : tokenData . expires_at * 1000 ,
79+ ...( info . enterpriseUrl && { enterpriseUrl : info . enterpriseUrl } ) ,
10180 } ,
10281 } ) ;
10382 info . access = tokenData . token ;
@@ -138,32 +117,65 @@ export async function CopilotAuthPlugin({ client }) {
138117 } ,
139118 } ;
140119 } ,
141- methods : [
142- {
143- label : "Login with GitHub" ,
144- type : "oauth" ,
145- authorize : async ( ) => {
146- // During authorize, read from config only (no auth data exists yet)
147- const urls = await getUrls ( "github-copilot" , null ) ;
148- const deviceResponse = await fetch ( urls . DEVICE_CODE_URL , {
149- method : "POST" ,
150- headers : {
151- Accept : "application/json" ,
152- "Content-Type" : "application/json" ,
153- "User-Agent" : "GitHubCopilotChat/0.35.0" ,
154- } ,
155- body : JSON . stringify ( {
156- client_id : CLIENT_ID ,
157- scope : "read:user" ,
158- } ) ,
159- } ) ;
160- const deviceData = await deviceResponse . json ( ) ;
161- return {
162- url : deviceData . verification_uri ,
163- instructions : `Enter code: ${ deviceData . user_code } ` ,
164- method : "auto" ,
165- callback : async ( ) => {
120+ methods : isEnterprise
121+ ? [
122+ {
123+ type : "custom" ,
124+ label : "Login with GitHub Enterprise" ,
125+ prompts : [
126+ {
127+ key : "enterpriseUrl" ,
128+ message : "Enter your GitHub Enterprise URL or domain" ,
129+ placeholder : "github.company.com or https://github.company.com" ,
130+ validate : ( value ) => {
131+ if ( ! value ) return "URL or domain is required" ;
132+ try {
133+ const url = value . includes ( "://" )
134+ ? new URL ( value )
135+ : new URL ( `https://${ value } ` ) ;
136+ if ( ! url . hostname )
137+ return "Please enter a valid URL or domain" ;
138+ return undefined ;
139+ } catch {
140+ return "Please enter a valid URL (e.g., github.company.com)" ;
141+ }
142+ } ,
143+ } ,
144+ ] ,
145+ async authorize ( inputs ) {
146+ const enterpriseUrl = inputs . enterpriseUrl ;
147+ const domain = normalizeDomain ( enterpriseUrl ) ;
148+ const urls = getUrls ( domain ) ;
149+
150+ const deviceResponse = await fetch ( urls . DEVICE_CODE_URL , {
151+ method : "POST" ,
152+ headers : {
153+ Accept : "application/json" ,
154+ "Content-Type" : "application/json" ,
155+ "User-Agent" : "GitHubCopilotChat/0.35.0" ,
156+ } ,
157+ body : JSON . stringify ( {
158+ client_id : CLIENT_ID ,
159+ scope : "read:user" ,
160+ } ) ,
161+ } ) ;
162+
163+ if ( ! deviceResponse . ok ) {
164+ return { type : "failed" } ;
165+ }
166+
167+ const deviceData = await deviceResponse . json ( ) ;
168+
169+ // Display URL and code for user
170+ console . log ( `Go to: ${ deviceData . verification_uri } ` ) ;
171+ console . log ( `Enter code: ${ deviceData . user_code } ` ) ;
172+
173+ // Poll for authorization
166174 while ( true ) {
175+ await new Promise ( ( resolve ) =>
176+ setTimeout ( resolve , ( deviceData . interval || 5 ) * 1000 ) ,
177+ ) ;
178+
167179 const response = await fetch ( urls . ACCESS_TOKEN_URL , {
168180 method : "POST" ,
169181 headers : {
@@ -186,31 +198,116 @@ export async function CopilotAuthPlugin({ client }) {
186198 if ( data . access_token ) {
187199 return {
188200 type : "success" ,
201+ auth_type : "oauth" ,
189202 refresh : data . access_token ,
190203 access : "" ,
191204 expires : 0 ,
205+ enterpriseUrl : domain ,
192206 } ;
193207 }
194208
195209 if ( data . error === "authorization_pending" ) {
196- await new Promise ( ( resolve ) =>
197- setTimeout ( resolve , deviceData . interval * 1000 ) ,
198- ) ;
199210 continue ;
200211 }
201212
202213 if ( data . error ) return { type : "failed" } ;
203-
204- await new Promise ( ( resolve ) =>
205- setTimeout ( resolve , deviceData . interval * 1000 ) ,
206- ) ;
207- continue ;
208214 }
209215 } ,
210- } ;
211- } ,
212- } ,
213- ] ,
216+ } ,
217+ ]
218+ : [
219+ {
220+ type : "oauth" ,
221+ label : "Login with GitHub" ,
222+ authorize : async ( ) => {
223+ const urls = getUrls ( "github.com" ) ;
224+
225+ const deviceResponse = await fetch ( urls . DEVICE_CODE_URL , {
226+ method : "POST" ,
227+ headers : {
228+ Accept : "application/json" ,
229+ "Content-Type" : "application/json" ,
230+ "User-Agent" : "GitHubCopilotChat/0.35.0" ,
231+ } ,
232+ body : JSON . stringify ( {
233+ client_id : CLIENT_ID ,
234+ scope : "read:user" ,
235+ } ) ,
236+ } ) ;
237+ const deviceData = await deviceResponse . json ( ) ;
238+ return {
239+ url : deviceData . verification_uri ,
240+ instructions : `Enter code: ${ deviceData . user_code } ` ,
241+ method : "auto" ,
242+ callback : async ( ) => {
243+ while ( true ) {
244+ const response = await fetch ( urls . ACCESS_TOKEN_URL , {
245+ method : "POST" ,
246+ headers : {
247+ Accept : "application/json" ,
248+ "Content-Type" : "application/json" ,
249+ "User-Agent" : "GitHubCopilotChat/0.35.0" ,
250+ } ,
251+ body : JSON . stringify ( {
252+ client_id : CLIENT_ID ,
253+ device_code : deviceData . device_code ,
254+ grant_type :
255+ "urn:ietf:params:oauth:grant-type:device_code" ,
256+ } ) ,
257+ } ) ;
258+
259+ if ( ! response . ok ) return { type : "failed" } ;
260+
261+ const data = await response . json ( ) ;
262+
263+ if ( data . access_token ) {
264+ return {
265+ type : "success" ,
266+ refresh : data . access_token ,
267+ access : "" ,
268+ expires : 0 ,
269+ } ;
270+ }
271+
272+ if ( data . error === "authorization_pending" ) {
273+ await new Promise ( ( resolve ) =>
274+ setTimeout ( resolve , deviceData . interval * 1000 ) ,
275+ ) ;
276+ continue ;
277+ }
278+
279+ if ( data . error ) return { type : "failed" } ;
280+
281+ await new Promise ( ( resolve ) =>
282+ setTimeout ( resolve , deviceData . interval * 1000 ) ,
283+ ) ;
284+ continue ;
285+ }
286+ } ,
287+ } ;
288+ } ,
289+ } ,
290+ ] ,
214291 } ,
215292 } ;
216293}
294+
295+ /**
296+ * @type {import('@opencode-ai/plugin').Plugin }
297+ */
298+ export async function CopilotAuthPlugin ( { client } ) {
299+ return createCopilotPlugin ( client , {
300+ providerId : "github-copilot" ,
301+ isEnterprise : false ,
302+ } ) ;
303+ }
304+
305+ /**
306+ * @type {import('@opencode-ai/plugin').Plugin }
307+ */
308+ export async function CopilotEnterpriseAuthPlugin ( { client } ) {
309+ return createCopilotPlugin ( client , {
310+ providerId : "github-copilot-enterprise" ,
311+ isEnterprise : true ,
312+ } ) ;
313+ }
0 commit comments