Skip to content

Commit 0105574

Browse files
committed
sync
1 parent b920807 commit 0105574

4 files changed

Lines changed: 199 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

bun.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.mjs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* @type {import('@opencode-ai/plugin').Plugin}
3+
*/
4+
export async function CopilotAuthPlugin({ client }) {
5+
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
6+
const DEVICE_CODE_URL = "https://github.com/login/device/code";
7+
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
8+
const COPILOT_API_KEY_URL =
9+
"https://api.github.com/copilot_internal/v2/token";
10+
const HEADERS = {
11+
"User-Agent": "GitHubCopilotChat/0.26.7",
12+
"Editor-Version": "vscode/1.99.3",
13+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
14+
"Copilot-Integration-Id": "vscode-chat",
15+
};
16+
17+
return {
18+
auth: {
19+
provider: "github-copilot",
20+
loader: async (getAuth, provider) => {
21+
let info = await getAuth();
22+
if (!info || info.type !== "oauth") return {};
23+
24+
if (provider && provider.models) {
25+
for (const model of Object.values(provider.models)) {
26+
model.cost = {
27+
input: 0,
28+
output: 0,
29+
};
30+
}
31+
}
32+
33+
return {
34+
apiKey: "",
35+
async fetch(input, init) {
36+
const info = await getAuth();
37+
if (info.type !== "oauth") return {};
38+
if (!info.access || info.expires < Date.now()) {
39+
const response = await fetch(COPILOT_API_KEY_URL, {
40+
headers: {
41+
Accept: "application/json",
42+
Authorization: `Bearer ${info.refresh}`,
43+
...HEADERS,
44+
},
45+
});
46+
47+
if (!response.ok) return;
48+
49+
const tokenData = await response.json();
50+
51+
await client.auth.set({
52+
path: {
53+
id: provider.id,
54+
},
55+
body: {
56+
type: "oauth",
57+
refresh: info.refresh,
58+
access: tokenData.token,
59+
expires: tokenData.expires_at * 1000,
60+
},
61+
});
62+
info.access = tokenData.token;
63+
}
64+
let isAgentCall = false;
65+
let isVisionRequest = false;
66+
try {
67+
const body =
68+
typeof init.body === "string"
69+
? JSON.parse(init.body)
70+
: init.body;
71+
if (body?.messages) {
72+
isAgentCall = body.messages.some(
73+
(msg) => msg.role && ["tool", "assistant"].includes(msg.role),
74+
);
75+
isVisionRequest = body.messages.some(
76+
(msg) =>
77+
Array.isArray(msg.content) &&
78+
msg.content.some((part) => part.type === "image_url"),
79+
);
80+
}
81+
} catch {}
82+
const headers = {
83+
...init.headers,
84+
...HEADERS,
85+
Authorization: `Bearer ${info.access}`,
86+
"Openai-Intent": "conversation-edits",
87+
"X-Initiator": isAgentCall ? "agent" : "user",
88+
};
89+
if (isVisionRequest) {
90+
headers["Copilot-Vision-Request"] = "true";
91+
}
92+
delete headers["x-api-key"];
93+
return fetch(input, {
94+
...init,
95+
headers,
96+
});
97+
},
98+
};
99+
},
100+
methods: [
101+
{
102+
label: "Login with GitHub",
103+
type: "oauth",
104+
authorize: async () => {
105+
const deviceResponse = await fetch(DEVICE_CODE_URL, {
106+
method: "POST",
107+
headers: {
108+
Accept: "application/json",
109+
"Content-Type": "application/json",
110+
"User-Agent": "GitHubCopilotChat/0.26.7",
111+
},
112+
body: JSON.stringify({
113+
client_id: CLIENT_ID,
114+
scope: "read:user",
115+
}),
116+
});
117+
const deviceData = await deviceResponse.json();
118+
return {
119+
url: deviceData.verification_uri,
120+
instructions: `Enter code: ${deviceData.user_code}`,
121+
method: "auto",
122+
callback: async () => {
123+
while (true) {
124+
const response = await fetch(ACCESS_TOKEN_URL, {
125+
method: "POST",
126+
headers: {
127+
Accept: "application/json",
128+
"Content-Type": "application/json",
129+
"User-Agent": "GitHubCopilotChat/0.26.7",
130+
},
131+
body: JSON.stringify({
132+
client_id: CLIENT_ID,
133+
device_code: deviceData.device_code,
134+
grant_type:
135+
"urn:ietf:params:oauth:grant-type:device_code",
136+
}),
137+
});
138+
139+
if (!response.ok) return { type: "failed" };
140+
141+
const data = await response.json();
142+
143+
if (data.access_token) {
144+
return {
145+
type: "success",
146+
refresh: data.access_token,
147+
access: "",
148+
expires: 0,
149+
};
150+
}
151+
152+
if (data.error === "authorization_pending") {
153+
await new Promise((resolve) =>
154+
setTimeout(resolve, deviceData.interval * 1000),
155+
);
156+
continue;
157+
}
158+
159+
if (data.error) return { type: "failed" };
160+
161+
await new Promise((resolve) =>
162+
setTimeout(resolve, deviceData.interval * 1000),
163+
);
164+
continue;
165+
}
166+
},
167+
};
168+
},
169+
},
170+
],
171+
},
172+
};
173+
}

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "opencode-copilot-auth",
3+
"version": "0.0.1",
4+
"main": "./index.mjs",
5+
"devDependencies": {
6+
"@opencode-ai/plugin": "^0.4.45"
7+
},
8+
"dependencies": {}
9+
}

0 commit comments

Comments
 (0)