Skip to content

Commit e2fc6fb

Browse files
committed
feat: unified auth flow with deployment type selection
- Combine separate auth methods into single plugin with select prompts - Add deployment type selection: GitHub.com (Public) or GitHub Enterprise - Use conditional prompts to only ask for enterprise URL when needed - Automatically save enterprise auth to github-copilot-enterprise provider
1 parent 98c9d11 commit e2fc6fb

1 file changed

Lines changed: 113 additions & 172 deletions

File tree

index.mjs

Lines changed: 113 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
function createCopilotPlugin(client, { providerId, isEnterprise }) {
1+
/**
2+
* @type {import('@opencode-ai/plugin').Plugin}
3+
*/
4+
export async function CopilotAuthPlugin({ client }) {
25
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
36
const HEADERS = {
47
"User-Agent": "GitHubCopilotChat/0.35.0",
@@ -23,7 +26,7 @@ function createCopilotPlugin(client, { providerId, isEnterprise }) {
2326

2427
return {
2528
auth: {
26-
provider: providerId,
29+
provider: "github-copilot",
2730
loader: async (getAuth, provider) => {
2831
let info = await getAuth();
2932
if (!info || info.type !== "oauth") return {};
@@ -117,197 +120,135 @@ function createCopilotPlugin(client, { providerId, isEnterprise }) {
117120
},
118121
};
119122
},
120-
methods: isEnterprise
121-
? [
123+
methods: [
124+
{
125+
type: "custom",
126+
label: "Login with GitHub Copilot",
127+
prompts: [
122128
{
123-
type: "custom",
124-
label: "Login with GitHub Enterprise",
125-
prompts: [
129+
type: "select",
130+
key: "deploymentType",
131+
message: "Select GitHub deployment type",
132+
options: [
133+
{
134+
label: "GitHub.com",
135+
value: "github.com",
136+
hint: "Public",
137+
},
126138
{
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-
},
139+
label: "GitHub Enterprise",
140+
value: "enterprise",
141+
hint: "Data residency or self-hosted",
143142
},
144143
],
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" };
144+
},
145+
{
146+
type: "text",
147+
key: "enterpriseUrl",
148+
message: "Enter your GitHub Enterprise URL or domain",
149+
placeholder: "company.ghe.com or https://company.ghe.com",
150+
condition: (inputs) => inputs.deploymentType === "enterprise",
151+
validate: (value) => {
152+
if (!value) return "URL or domain is required";
153+
try {
154+
const url = value.includes("://")
155+
? new URL(value)
156+
: new URL(`https://${value}`);
157+
if (!url.hostname)
158+
return "Please enter a valid URL or domain";
159+
return undefined;
160+
} catch {
161+
return "Please enter a valid URL (e.g., company.ghe.com or https://company.ghe.com)";
165162
}
163+
},
164+
},
165+
],
166+
async authorize(inputs) {
167+
const deploymentType = inputs.deploymentType;
166168

167-
const deviceData = await deviceResponse.json();
169+
let domain = "github.com";
170+
let actualProvider = "github-copilot";
168171

169-
// Display URL and code for user
170-
console.log(`Go to: ${deviceData.verification_uri}`);
171-
console.log(`Enter code: ${deviceData.user_code}`);
172+
if (deploymentType === "enterprise") {
173+
const enterpriseUrl = inputs.enterpriseUrl;
174+
domain = normalizeDomain(enterpriseUrl);
175+
actualProvider = "github-copilot-enterprise";
176+
}
172177

173-
// Poll for authorization
174-
while (true) {
175-
await new Promise((resolve) =>
176-
setTimeout(resolve, (deviceData.interval || 5) * 1000),
177-
);
178+
const urls = getUrls(domain);
178179

179-
const response = await fetch(urls.ACCESS_TOKEN_URL, {
180-
method: "POST",
181-
headers: {
182-
Accept: "application/json",
183-
"Content-Type": "application/json",
184-
"User-Agent": "GitHubCopilotChat/0.35.0",
185-
},
186-
body: JSON.stringify({
187-
client_id: CLIENT_ID,
188-
device_code: deviceData.device_code,
189-
grant_type:
190-
"urn:ietf:params:oauth:grant-type:device_code",
191-
}),
192-
});
180+
const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
181+
method: "POST",
182+
headers: {
183+
Accept: "application/json",
184+
"Content-Type": "application/json",
185+
"User-Agent": "GitHubCopilotChat/0.35.0",
186+
},
187+
body: JSON.stringify({
188+
client_id: CLIENT_ID,
189+
scope: "read:user",
190+
}),
191+
});
193192

194-
if (!response.ok) return { type: "failed" };
193+
if (!deviceResponse.ok) {
194+
return { type: "failed" };
195+
}
195196

196-
const data = await response.json();
197+
const deviceData = await deviceResponse.json();
197198

198-
if (data.access_token) {
199-
return {
200-
type: "success",
201-
auth_type: "oauth",
202-
refresh: data.access_token,
203-
access: "",
204-
expires: 0,
205-
enterpriseUrl: domain,
206-
};
207-
}
199+
console.log(`Go to: ${deviceData.verification_uri}`);
200+
console.log(`Enter code: ${deviceData.user_code}`);
208201

209-
if (data.error === "authorization_pending") {
210-
continue;
211-
}
202+
while (true) {
203+
await new Promise((resolve) =>
204+
setTimeout(resolve, (deviceData.interval || 5) * 1000),
205+
);
212206

213-
if (data.error) return { type: "failed" };
214-
}
215-
},
216-
},
217-
]
218-
: [
219-
{
220-
type: "oauth",
221-
label: "Login with GitHub",
222-
authorize: async () => {
223-
const urls = getUrls("github.com");
207+
const response = await fetch(urls.ACCESS_TOKEN_URL, {
208+
method: "POST",
209+
headers: {
210+
Accept: "application/json",
211+
"Content-Type": "application/json",
212+
"User-Agent": "GitHubCopilotChat/0.35.0",
213+
},
214+
body: JSON.stringify({
215+
client_id: CLIENT_ID,
216+
device_code: deviceData.device_code,
217+
grant_type:
218+
"urn:ietf:params:oauth:grant-type:device_code",
219+
}),
220+
});
224221

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-
});
222+
if (!response.ok) return { type: "failed" };
258223

259-
if (!response.ok) return { type: "failed" };
224+
const data = await response.json();
260225

261-
const data = await response.json();
226+
if (data.access_token) {
227+
const result = {
228+
type: "success",
229+
auth_type: "oauth",
230+
refresh: data.access_token,
231+
access: "",
232+
expires: 0,
233+
};
262234

263-
if (data.access_token) {
264-
return {
265-
type: "success",
266-
refresh: data.access_token,
267-
access: "",
268-
expires: 0,
269-
};
270-
}
235+
if (actualProvider === "github-copilot-enterprise") {
236+
result.provider = "github-copilot-enterprise";
237+
result.enterpriseUrl = domain;
238+
}
271239

272-
if (data.error === "authorization_pending") {
273-
await new Promise((resolve) =>
274-
setTimeout(resolve, deviceData.interval * 1000),
275-
);
276-
continue;
277-
}
240+
return result;
241+
}
278242

279-
if (data.error) return { type: "failed" };
243+
if (data.error === "authorization_pending") {
244+
continue;
245+
}
280246

281-
await new Promise((resolve) =>
282-
setTimeout(resolve, deviceData.interval * 1000),
283-
);
284-
continue;
285-
}
286-
},
287-
};
288-
},
289-
},
290-
],
247+
if (data.error) return { type: "failed" };
248+
}
249+
},
250+
},
251+
],
291252
},
292253
};
293254
}
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

Comments
 (0)