@draht/ai 2026.3.2-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1185 -0
- package/dist/api-registry.d.ts +20 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +44 -0
- package/dist/api-registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +116 -0
- package/dist/cli.js.map +1 -0
- package/dist/env-api-keys.d.ts +9 -0
- package/dist/env-api-keys.d.ts.map +1 -0
- package/dist/env-api-keys.js +99 -0
- package/dist/env-api-keys.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +24 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.generated.d.ts +13133 -0
- package/dist/models.generated.d.ts.map +1 -0
- package/dist/models.generated.js +12939 -0
- package/dist/models.generated.js.map +1 -0
- package/dist/models.js +55 -0
- package/dist/models.js.map +1 -0
- package/dist/providers/amazon-bedrock.d.ts +15 -0
- package/dist/providers/amazon-bedrock.d.ts.map +1 -0
- package/dist/providers/amazon-bedrock.js +585 -0
- package/dist/providers/amazon-bedrock.js.map +1 -0
- package/dist/providers/anthropic.d.ts +33 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +729 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/azure-openai-responses.d.ts +15 -0
- package/dist/providers/azure-openai-responses.d.ts.map +1 -0
- package/dist/providers/azure-openai-responses.js +184 -0
- package/dist/providers/azure-openai-responses.js.map +1 -0
- package/dist/providers/github-copilot-headers.d.ts +8 -0
- package/dist/providers/github-copilot-headers.d.ts.map +1 -0
- package/dist/providers/github-copilot-headers.js +29 -0
- package/dist/providers/github-copilot-headers.js.map +1 -0
- package/dist/providers/google-gemini-cli.d.ts +74 -0
- package/dist/providers/google-gemini-cli.d.ts.map +1 -0
- package/dist/providers/google-gemini-cli.js +735 -0
- package/dist/providers/google-gemini-cli.js.map +1 -0
- package/dist/providers/google-shared.d.ts +65 -0
- package/dist/providers/google-shared.d.ts.map +1 -0
- package/dist/providers/google-shared.js +306 -0
- package/dist/providers/google-shared.js.map +1 -0
- package/dist/providers/google-vertex.d.ts +15 -0
- package/dist/providers/google-vertex.d.ts.map +1 -0
- package/dist/providers/google-vertex.js +371 -0
- package/dist/providers/google-vertex.js.map +1 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +352 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/openai-codex-responses.d.ts +9 -0
- package/dist/providers/openai-codex-responses.d.ts.map +1 -0
- package/dist/providers/openai-codex-responses.js +699 -0
- package/dist/providers/openai-codex-responses.js.map +1 -0
- package/dist/providers/openai-completions.d.ts +15 -0
- package/dist/providers/openai-completions.d.ts.map +1 -0
- package/dist/providers/openai-completions.js +712 -0
- package/dist/providers/openai-completions.js.map +1 -0
- package/dist/providers/openai-responses-shared.d.ts +17 -0
- package/dist/providers/openai-responses-shared.d.ts.map +1 -0
- package/dist/providers/openai-responses-shared.js +427 -0
- package/dist/providers/openai-responses-shared.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +13 -0
- package/dist/providers/openai-responses.d.ts.map +1 -0
- package/dist/providers/openai-responses.js +198 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/register-builtins.d.ts +3 -0
- package/dist/providers/register-builtins.d.ts.map +1 -0
- package/dist/providers/register-builtins.js +63 -0
- package/dist/providers/register-builtins.js.map +1 -0
- package/dist/providers/simple-options.d.ts +8 -0
- package/dist/providers/simple-options.d.ts.map +1 -0
- package/dist/providers/simple-options.js +35 -0
- package/dist/providers/simple-options.js.map +1 -0
- package/dist/providers/transform-messages.d.ts +8 -0
- package/dist/providers/transform-messages.d.ts.map +1 -0
- package/dist/providers/transform-messages.js +155 -0
- package/dist/providers/transform-messages.js.map +1 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +28 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/event-stream.d.ts +21 -0
- package/dist/utils/event-stream.d.ts.map +1 -0
- package/dist/utils/event-stream.js +81 -0
- package/dist/utils/event-stream.js.map +1 -0
- package/dist/utils/http-proxy.d.ts +2 -0
- package/dist/utils/http-proxy.d.ts.map +1 -0
- package/dist/utils/http-proxy.js +15 -0
- package/dist/utils/http-proxy.js.map +1 -0
- package/dist/utils/json-parse.d.ts +9 -0
- package/dist/utils/json-parse.d.ts.map +1 -0
- package/dist/utils/json-parse.js +29 -0
- package/dist/utils/json-parse.js.map +1 -0
- package/dist/utils/oauth/anthropic.d.ts +17 -0
- package/dist/utils/oauth/anthropic.d.ts.map +1 -0
- package/dist/utils/oauth/anthropic.js +104 -0
- package/dist/utils/oauth/anthropic.js.map +1 -0
- package/dist/utils/oauth/github-copilot.d.ts +30 -0
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -0
- package/dist/utils/oauth/github-copilot.js +281 -0
- package/dist/utils/oauth/github-copilot.js.map +1 -0
- package/dist/utils/oauth/google-antigravity.d.ts +26 -0
- package/dist/utils/oauth/google-antigravity.d.ts.map +1 -0
- package/dist/utils/oauth/google-antigravity.js +373 -0
- package/dist/utils/oauth/google-antigravity.js.map +1 -0
- package/dist/utils/oauth/google-gemini-cli.d.ts +26 -0
- package/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -0
- package/dist/utils/oauth/google-gemini-cli.js +478 -0
- package/dist/utils/oauth/google-gemini-cli.js.map +1 -0
- package/dist/utils/oauth/index.d.ts +62 -0
- package/dist/utils/oauth/index.d.ts.map +1 -0
- package/dist/utils/oauth/index.js +133 -0
- package/dist/utils/oauth/index.js.map +1 -0
- package/dist/utils/oauth/openai-codex.d.ts +34 -0
- package/dist/utils/oauth/openai-codex.d.ts.map +1 -0
- package/dist/utils/oauth/openai-codex.js +380 -0
- package/dist/utils/oauth/openai-codex.js.map +1 -0
- package/dist/utils/oauth/pkce.d.ts +13 -0
- package/dist/utils/oauth/pkce.d.ts.map +1 -0
- package/dist/utils/oauth/pkce.js +31 -0
- package/dist/utils/oauth/pkce.js.map +1 -0
- package/dist/utils/oauth/types.d.ts +47 -0
- package/dist/utils/oauth/types.d.ts.map +1 -0
- package/dist/utils/oauth/types.js +2 -0
- package/dist/utils/oauth/types.js.map +1 -0
- package/dist/utils/overflow.d.ts +52 -0
- package/dist/utils/overflow.d.ts.map +1 -0
- package/dist/utils/overflow.js +115 -0
- package/dist/utils/overflow.js.map +1 -0
- package/dist/utils/sanitize-unicode.d.ts +22 -0
- package/dist/utils/sanitize-unicode.d.ts.map +1 -0
- package/dist/utils/sanitize-unicode.js +26 -0
- package/dist/utils/sanitize-unicode.js.map +1 -0
- package/dist/utils/typebox-helpers.d.ts +17 -0
- package/dist/utils/typebox-helpers.d.ts.map +1 -0
- package/dist/utils/typebox-helpers.js +21 -0
- package/dist/utils/typebox-helpers.js.map +1 -0
- package/dist/utils/validation.d.ts +18 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +72 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot OAuth flow
|
|
3
|
+
*/
|
|
4
|
+
import { getModels } from "../../models.js";
|
|
5
|
+
const decode = (s) => atob(s);
|
|
6
|
+
const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
|
|
7
|
+
const COPILOT_HEADERS = {
|
|
8
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
9
|
+
"Editor-Version": "vscode/1.107.0",
|
|
10
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
11
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
12
|
+
};
|
|
13
|
+
export function normalizeDomain(input) {
|
|
14
|
+
const trimmed = input.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
|
|
19
|
+
return url.hostname;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getUrls(domain) {
|
|
26
|
+
return {
|
|
27
|
+
deviceCodeUrl: `https://${domain}/login/device/code`,
|
|
28
|
+
accessTokenUrl: `https://${domain}/login/oauth/access_token`,
|
|
29
|
+
copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse the proxy-ep from a Copilot token and convert to API base URL.
|
|
34
|
+
* Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
|
|
35
|
+
* Returns API URL like https://api.individual.githubcopilot.com
|
|
36
|
+
*/
|
|
37
|
+
function getBaseUrlFromToken(token) {
|
|
38
|
+
const match = token.match(/proxy-ep=([^;]+)/);
|
|
39
|
+
if (!match)
|
|
40
|
+
return null;
|
|
41
|
+
const proxyHost = match[1];
|
|
42
|
+
// Convert proxy.xxx to api.xxx
|
|
43
|
+
const apiHost = proxyHost.replace(/^proxy\./, "api.");
|
|
44
|
+
return `https://${apiHost}`;
|
|
45
|
+
}
|
|
46
|
+
export function getGitHubCopilotBaseUrl(token, enterpriseDomain) {
|
|
47
|
+
// If we have a token, extract the base URL from proxy-ep
|
|
48
|
+
if (token) {
|
|
49
|
+
const urlFromToken = getBaseUrlFromToken(token);
|
|
50
|
+
if (urlFromToken)
|
|
51
|
+
return urlFromToken;
|
|
52
|
+
}
|
|
53
|
+
// Fallback for enterprise or if token parsing fails
|
|
54
|
+
if (enterpriseDomain)
|
|
55
|
+
return `https://copilot-api.${enterpriseDomain}`;
|
|
56
|
+
return "https://api.individual.githubcopilot.com";
|
|
57
|
+
}
|
|
58
|
+
async function fetchJson(url, init) {
|
|
59
|
+
const response = await fetch(url, init);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
throw new Error(`${response.status} ${response.statusText}: ${text}`);
|
|
63
|
+
}
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
66
|
+
async function startDeviceFlow(domain) {
|
|
67
|
+
const urls = getUrls(domain);
|
|
68
|
+
const data = await fetchJson(urls.deviceCodeUrl, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
client_id: CLIENT_ID,
|
|
77
|
+
scope: "read:user",
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
if (!data || typeof data !== "object") {
|
|
81
|
+
throw new Error("Invalid device code response");
|
|
82
|
+
}
|
|
83
|
+
const deviceCode = data.device_code;
|
|
84
|
+
const userCode = data.user_code;
|
|
85
|
+
const verificationUri = data.verification_uri;
|
|
86
|
+
const interval = data.interval;
|
|
87
|
+
const expiresIn = data.expires_in;
|
|
88
|
+
if (typeof deviceCode !== "string" ||
|
|
89
|
+
typeof userCode !== "string" ||
|
|
90
|
+
typeof verificationUri !== "string" ||
|
|
91
|
+
typeof interval !== "number" ||
|
|
92
|
+
typeof expiresIn !== "number") {
|
|
93
|
+
throw new Error("Invalid device code response fields");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
device_code: deviceCode,
|
|
97
|
+
user_code: userCode,
|
|
98
|
+
verification_uri: verificationUri,
|
|
99
|
+
interval,
|
|
100
|
+
expires_in: expiresIn,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sleep that can be interrupted by an AbortSignal
|
|
105
|
+
*/
|
|
106
|
+
function abortableSleep(ms, signal) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
if (signal?.aborted) {
|
|
109
|
+
reject(new Error("Login cancelled"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const timeout = setTimeout(resolve, ms);
|
|
113
|
+
signal?.addEventListener("abort", () => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
reject(new Error("Login cancelled"));
|
|
116
|
+
}, { once: true });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function pollForGitHubAccessToken(domain, deviceCode, intervalSeconds, expiresIn, signal) {
|
|
120
|
+
const urls = getUrls(domain);
|
|
121
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
122
|
+
let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
if (signal?.aborted) {
|
|
125
|
+
throw new Error("Login cancelled");
|
|
126
|
+
}
|
|
127
|
+
const raw = await fetchJson(urls.accessTokenUrl, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
Accept: "application/json",
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
client_id: CLIENT_ID,
|
|
136
|
+
device_code: deviceCode,
|
|
137
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
if (raw && typeof raw === "object" && typeof raw.access_token === "string") {
|
|
141
|
+
return raw.access_token;
|
|
142
|
+
}
|
|
143
|
+
if (raw && typeof raw === "object" && typeof raw.error === "string") {
|
|
144
|
+
const err = raw.error;
|
|
145
|
+
if (err === "authorization_pending") {
|
|
146
|
+
await abortableSleep(intervalMs, signal);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (err === "slow_down") {
|
|
150
|
+
intervalMs += 5000;
|
|
151
|
+
await abortableSleep(intervalMs, signal);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Device flow failed: ${err}`);
|
|
155
|
+
}
|
|
156
|
+
await abortableSleep(intervalMs, signal);
|
|
157
|
+
}
|
|
158
|
+
throw new Error("Device flow timed out");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Refresh GitHub Copilot token
|
|
162
|
+
*/
|
|
163
|
+
export async function refreshGitHubCopilotToken(refreshToken, enterpriseDomain) {
|
|
164
|
+
const domain = enterpriseDomain || "github.com";
|
|
165
|
+
const urls = getUrls(domain);
|
|
166
|
+
const raw = await fetchJson(urls.copilotTokenUrl, {
|
|
167
|
+
headers: {
|
|
168
|
+
Accept: "application/json",
|
|
169
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
170
|
+
...COPILOT_HEADERS,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!raw || typeof raw !== "object") {
|
|
174
|
+
throw new Error("Invalid Copilot token response");
|
|
175
|
+
}
|
|
176
|
+
const token = raw.token;
|
|
177
|
+
const expiresAt = raw.expires_at;
|
|
178
|
+
if (typeof token !== "string" || typeof expiresAt !== "number") {
|
|
179
|
+
throw new Error("Invalid Copilot token response fields");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
refresh: refreshToken,
|
|
183
|
+
access: token,
|
|
184
|
+
expires: expiresAt * 1000 - 5 * 60 * 1000,
|
|
185
|
+
enterpriseUrl: enterpriseDomain,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Enable a model for the user's GitHub Copilot account.
|
|
190
|
+
* This is required for some models (like Claude, Grok) before they can be used.
|
|
191
|
+
*/
|
|
192
|
+
async function enableGitHubCopilotModel(token, modelId, enterpriseDomain) {
|
|
193
|
+
const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
|
|
194
|
+
const url = `${baseUrl}/models/${modelId}/policy`;
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
Authorization: `Bearer ${token}`,
|
|
201
|
+
...COPILOT_HEADERS,
|
|
202
|
+
"openai-intent": "chat-policy",
|
|
203
|
+
"x-interaction-type": "chat-policy",
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({ state: "enabled" }),
|
|
206
|
+
});
|
|
207
|
+
return response.ok;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Enable all known GitHub Copilot models that may require policy acceptance.
|
|
215
|
+
* Called after successful login to ensure all models are available.
|
|
216
|
+
*/
|
|
217
|
+
async function enableAllGitHubCopilotModels(token, enterpriseDomain, onProgress) {
|
|
218
|
+
const models = getModels("github-copilot");
|
|
219
|
+
await Promise.all(models.map(async (model) => {
|
|
220
|
+
const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
|
|
221
|
+
onProgress?.(model.id, success);
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Login with GitHub Copilot OAuth (device code flow)
|
|
226
|
+
*
|
|
227
|
+
* @param options.onAuth - Callback with URL and optional instructions (user code)
|
|
228
|
+
* @param options.onPrompt - Callback to prompt user for input
|
|
229
|
+
* @param options.onProgress - Optional progress callback
|
|
230
|
+
* @param options.signal - Optional AbortSignal for cancellation
|
|
231
|
+
*/
|
|
232
|
+
export async function loginGitHubCopilot(options) {
|
|
233
|
+
const input = await options.onPrompt({
|
|
234
|
+
message: "GitHub Enterprise URL/domain (blank for github.com)",
|
|
235
|
+
placeholder: "company.ghe.com",
|
|
236
|
+
allowEmpty: true,
|
|
237
|
+
});
|
|
238
|
+
if (options.signal?.aborted) {
|
|
239
|
+
throw new Error("Login cancelled");
|
|
240
|
+
}
|
|
241
|
+
const trimmed = input.trim();
|
|
242
|
+
const enterpriseDomain = normalizeDomain(input);
|
|
243
|
+
if (trimmed && !enterpriseDomain) {
|
|
244
|
+
throw new Error("Invalid GitHub Enterprise URL/domain");
|
|
245
|
+
}
|
|
246
|
+
const domain = enterpriseDomain || "github.com";
|
|
247
|
+
const device = await startDeviceFlow(domain);
|
|
248
|
+
options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
|
|
249
|
+
const githubAccessToken = await pollForGitHubAccessToken(domain, device.device_code, device.interval, device.expires_in, options.signal);
|
|
250
|
+
const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
|
|
251
|
+
// Enable all models after successful login
|
|
252
|
+
options.onProgress?.("Enabling models...");
|
|
253
|
+
await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
|
|
254
|
+
return credentials;
|
|
255
|
+
}
|
|
256
|
+
export const githubCopilotOAuthProvider = {
|
|
257
|
+
id: "github-copilot",
|
|
258
|
+
name: "GitHub Copilot",
|
|
259
|
+
async login(callbacks) {
|
|
260
|
+
return loginGitHubCopilot({
|
|
261
|
+
onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
|
|
262
|
+
onPrompt: callbacks.onPrompt,
|
|
263
|
+
onProgress: callbacks.onProgress,
|
|
264
|
+
signal: callbacks.signal,
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
async refreshToken(credentials) {
|
|
268
|
+
const creds = credentials;
|
|
269
|
+
return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);
|
|
270
|
+
},
|
|
271
|
+
getApiKey(credentials) {
|
|
272
|
+
return credentials.access;
|
|
273
|
+
},
|
|
274
|
+
modifyModels(models, credentials) {
|
|
275
|
+
const creds = credentials;
|
|
276
|
+
const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;
|
|
277
|
+
const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);
|
|
278
|
+
return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=github-copilot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-copilot.js","sourceRoot":"","sources":["../../../src/utils/oauth/github-copilot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot OAuth flow\n */\n\nimport { getModels } from \"../../models.js\";\nimport type { Api, Model } from \"../../types.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.js\";\n\ntype CopilotCredentials = OAuthCredentials & {\n\tenterpriseUrl?: string;\n};\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"SXYxLmI1MDdhMDhjODdlY2ZlOTg=\");\n\nconst COPILOT_HEADERS = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n} as const;\n\ntype DeviceCodeResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri: string;\n\tinterval: number;\n\texpires_in: number;\n};\n\ntype DeviceTokenSuccessResponse = {\n\taccess_token: string;\n\ttoken_type?: string;\n\tscope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n\terror: string;\n\terror_description?: string;\n\tinterval?: number;\n};\n\nexport function normalizeDomain(input: string): string | null {\n\tconst trimmed = input.trim();\n\tif (!trimmed) return null;\n\ttry {\n\t\tconst url = trimmed.includes(\"://\") ? new URL(trimmed) : new URL(`https://${trimmed}`);\n\t\treturn url.hostname;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction getUrls(domain: string): {\n\tdeviceCodeUrl: string;\n\taccessTokenUrl: string;\n\tcopilotTokenUrl: string;\n} {\n\treturn {\n\t\tdeviceCodeUrl: `https://${domain}/login/device/code`,\n\t\taccessTokenUrl: `https://${domain}/login/oauth/access_token`,\n\t\tcopilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n\t};\n}\n\n/**\n * Parse the proxy-ep from a Copilot token and convert to API base URL.\n * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...\n * Returns API URL like https://api.individual.githubcopilot.com\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n\tconst match = token.match(/proxy-ep=([^;]+)/);\n\tif (!match) return null;\n\tconst proxyHost = match[1];\n\t// Convert proxy.xxx to api.xxx\n\tconst apiHost = proxyHost.replace(/^proxy\\./, \"api.\");\n\treturn `https://${apiHost}`;\n}\n\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n\t// If we have a token, extract the base URL from proxy-ep\n\tif (token) {\n\t\tconst urlFromToken = getBaseUrlFromToken(token);\n\t\tif (urlFromToken) return urlFromToken;\n\t}\n\t// Fallback for enterprise or if token parsing fails\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn \"https://api.individual.githubcopilot.com\";\n}\n\nasync function fetchJson(url: string, init: RequestInit): Promise<unknown> {\n\tconst response = await fetch(url, init);\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`${response.status} ${response.statusText}: ${text}`);\n\t}\n\treturn response.json();\n}\n\nasync function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {\n\tconst urls = getUrls(domain);\n\tconst data = await fetchJson(urls.deviceCodeUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: \"read:user\",\n\t\t}),\n\t});\n\n\tif (!data || typeof data !== \"object\") {\n\t\tthrow new Error(\"Invalid device code response\");\n\t}\n\n\tconst deviceCode = (data as Record<string, unknown>).device_code;\n\tconst userCode = (data as Record<string, unknown>).user_code;\n\tconst verificationUri = (data as Record<string, unknown>).verification_uri;\n\tconst interval = (data as Record<string, unknown>).interval;\n\tconst expiresIn = (data as Record<string, unknown>).expires_in;\n\n\tif (\n\t\ttypeof deviceCode !== \"string\" ||\n\t\ttypeof userCode !== \"string\" ||\n\t\ttypeof verificationUri !== \"string\" ||\n\t\ttypeof interval !== \"number\" ||\n\t\ttypeof expiresIn !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\treturn {\n\t\tdevice_code: deviceCode,\n\t\tuser_code: userCode,\n\t\tverification_uri: verificationUri,\n\t\tinterval,\n\t\texpires_in: expiresIn,\n\t};\n}\n\n/**\n * Sleep that can be interrupted by an AbortSignal\n */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst timeout = setTimeout(resolve, ms);\n\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\nasync function pollForGitHubAccessToken(\n\tdomain: string,\n\tdeviceCode: string,\n\tintervalSeconds: number,\n\texpiresIn: number,\n\tsignal?: AbortSignal,\n) {\n\tconst urls = getUrls(domain);\n\tconst deadline = Date.now() + expiresIn * 1000;\n\tlet intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));\n\n\twhile (Date.now() < deadline) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\n\t\tconst raw = await fetchJson(urls.accessTokenUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\tdevice_code: deviceCode,\n\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t}),\n\t\t});\n\n\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenSuccessResponse).access_token === \"string\") {\n\t\t\treturn (raw as DeviceTokenSuccessResponse).access_token;\n\t\t}\n\n\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenErrorResponse).error === \"string\") {\n\t\t\tconst err = (raw as DeviceTokenErrorResponse).error;\n\t\t\tif (err === \"authorization_pending\") {\n\t\t\t\tawait abortableSleep(intervalMs, signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (err === \"slow_down\") {\n\t\t\t\tintervalMs += 5000;\n\t\t\t\tawait abortableSleep(intervalMs, signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tthrow new Error(`Device flow failed: ${err}`);\n\t\t}\n\n\t\tawait abortableSleep(intervalMs, signal);\n\t}\n\n\tthrow new Error(\"Device flow timed out\");\n}\n\n/**\n * Refresh GitHub Copilot token\n */\nexport async function refreshGitHubCopilotToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst domain = enterpriseDomain || \"github.com\";\n\tconst urls = getUrls(domain);\n\n\tconst raw = await fetchJson(urls.copilotTokenUrl, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${refreshToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid Copilot token response\");\n\t}\n\n\tconst token = (raw as Record<string, unknown>).token;\n\tconst expiresAt = (raw as Record<string, unknown>).expires_at;\n\n\tif (typeof token !== \"string\" || typeof expiresAt !== \"number\") {\n\t\tthrow new Error(\"Invalid Copilot token response fields\");\n\t}\n\n\treturn {\n\t\trefresh: refreshToken,\n\t\taccess: token,\n\t\texpires: expiresAt * 1000 - 5 * 60 * 1000,\n\t\tenterpriseUrl: enterpriseDomain,\n\t};\n}\n\n/**\n * Enable a model for the user's GitHub Copilot account.\n * This is required for some models (like Claude, Grok) before they can be used.\n */\nasync function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);\n\tconst url = `${baseUrl}/models/${modelId}/policy`;\n\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t...COPILOT_HEADERS,\n\t\t\t\t\"openai-intent\": \"chat-policy\",\n\t\t\t\t\"x-interaction-type\": \"chat-policy\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({ state: \"enabled\" }),\n\t\t});\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Enable all known GitHub Copilot models that may require policy acceptance.\n * Called after successful login to ensure all models are available.\n */\nasync function enableAllGitHubCopilotModels(\n\ttoken: string,\n\tenterpriseDomain?: string,\n\tonProgress?: (model: string, success: boolean) => void,\n): Promise<void> {\n\tconst models = getModels(\"github-copilot\");\n\tawait Promise.all(\n\t\tmodels.map(async (model) => {\n\t\t\tconst success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);\n\t\t\tonProgress?.(model.id, success);\n\t\t}),\n\t);\n}\n\n/**\n * Login with GitHub Copilot OAuth (device code flow)\n *\n * @param options.onAuth - Callback with URL and optional instructions (user code)\n * @param options.onPrompt - Callback to prompt user for input\n * @param options.onProgress - Optional progress callback\n * @param options.signal - Optional AbortSignal for cancellation\n */\nexport async function loginGitHubCopilot(options: {\n\tonAuth: (url: string, instructions?: string) => void;\n\tonPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst input = await options.onPrompt({\n\t\tmessage: \"GitHub Enterprise URL/domain (blank for github.com)\",\n\t\tplaceholder: \"company.ghe.com\",\n\t\tallowEmpty: true,\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new Error(\"Login cancelled\");\n\t}\n\n\tconst trimmed = input.trim();\n\tconst enterpriseDomain = normalizeDomain(input);\n\tif (trimmed && !enterpriseDomain) {\n\t\tthrow new Error(\"Invalid GitHub Enterprise URL/domain\");\n\t}\n\tconst domain = enterpriseDomain || \"github.com\";\n\n\tconst device = await startDeviceFlow(domain);\n\toptions.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);\n\n\tconst githubAccessToken = await pollForGitHubAccessToken(\n\t\tdomain,\n\t\tdevice.device_code,\n\t\tdevice.interval,\n\t\tdevice.expires_in,\n\t\toptions.signal,\n\t);\n\tconst credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);\n\n\t// Enable all models after successful login\n\toptions.onProgress?.(\"Enabling models...\");\n\tawait enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);\n\treturn credentials;\n}\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n\tid: \"github-copilot\",\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGitHubCopilot({\n\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\treturn refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\tconst domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);\n\t\treturn models.map((m) => (m.provider === \"github-copilot\" ? { ...m, baseUrl } : m));\n\t},\n};\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
|
3
|
+
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This module uses Node.js http.createServer for the OAuth callback.
|
|
6
|
+
* It is only intended for CLI use, not browser environments.
|
|
7
|
+
*/
|
|
8
|
+
import type { OAuthCredentials, OAuthProviderInterface } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Refresh Antigravity token
|
|
11
|
+
*/
|
|
12
|
+
export declare function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials>;
|
|
13
|
+
/**
|
|
14
|
+
* Login with Antigravity OAuth
|
|
15
|
+
*
|
|
16
|
+
* @param onAuth - Callback with URL and optional instructions
|
|
17
|
+
* @param onProgress - Optional progress callback
|
|
18
|
+
* @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.
|
|
19
|
+
* Races with browser callback - whichever completes first wins.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loginAntigravity(onAuth: (info: {
|
|
22
|
+
url: string;
|
|
23
|
+
instructions?: string;
|
|
24
|
+
}) => void, onProgress?: (message: string) => void, onManualCodeInput?: () => Promise<string>): Promise<OAuthCredentials>;
|
|
25
|
+
export declare const antigravityOAuthProvider: OAuthProviderInterface;
|
|
26
|
+
//# sourceMappingURL=google-antigravity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-antigravity.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/google-antigravity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAgOhG;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6BhH;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACrC,MAAM,EAAE,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,EAC9D,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACtC,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GACvC,OAAO,CAAC,gBAAgB,CAAC,CAyJ3B;AAED,eAAO,MAAM,wBAAwB,EAAE,sBAqBtC,CAAC","sourcesContent":["/**\n * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)\n * Uses different OAuth credentials than google-gemini-cli for access to additional models.\n *\n * NOTE: This module uses Node.js http.createServer for the OAuth callback.\n * It is only intended for CLI use, not browser environments.\n */\n\nimport type { Server } from \"node:http\";\nimport { generatePKCE } from \"./pkce.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.js\";\n\ntype AntigravityCredentials = OAuthCredentials & {\n\tprojectId: string;\n};\n\nlet _createServer: typeof import(\"node:http\").createServer | null = null;\nlet _httpImportPromise: Promise<void> | null = null;\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\t_httpImportPromise = import(\"node:http\").then((m) => {\n\t\t_createServer = m.createServer;\n\t});\n}\n\n// Antigravity OAuth credentials (different from Gemini CLI)\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\n\t\"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==\",\n);\nconst CLIENT_SECRET = decode(\"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=\");\nconst REDIRECT_URI = \"http://localhost:51121/oauth-callback\";\n\n// Antigravity requires additional scopes\nconst SCOPES = [\n\t\"https://www.googleapis.com/auth/cloud-platform\",\n\t\"https://www.googleapis.com/auth/userinfo.email\",\n\t\"https://www.googleapis.com/auth/userinfo.profile\",\n\t\"https://www.googleapis.com/auth/cclog\",\n\t\"https://www.googleapis.com/auth/experimentsandconfigs\",\n];\n\nconst AUTH_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\n// Fallback project ID when discovery fails\nconst DEFAULT_PROJECT_ID = \"rising-fact-p41fc\";\n\ntype CallbackServerInfo = {\n\tserver: Server;\n\tcancelWait: () => void;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n};\n\n/**\n * Start a local HTTP server to receive the OAuth callback\n */\nasync function getNodeCreateServer(): Promise<typeof import(\"node:http\").createServer> {\n\tif (_createServer) return _createServer;\n\tif (_httpImportPromise) {\n\t\tawait _httpImportPromise;\n\t}\n\tif (_createServer) return _createServer;\n\tthrow new Error(\"Antigravity OAuth is only available in Node.js environments\");\n}\n\nasync function startCallbackServer(): Promise<CallbackServerInfo> {\n\tconst createServer = await getNodeCreateServer();\n\n\treturn new Promise((resolve, reject) => {\n\t\tlet result: { code: string; state: string } | null = null;\n\t\tlet cancelled = false;\n\n\t\tconst server = createServer((req, res) => {\n\t\t\tconst url = new URL(req.url || \"\", `http://localhost:51121`);\n\n\t\t\tif (url.pathname === \"/oauth-callback\") {\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\t`<html><body><h1>Authentication Failed</h1><p>Error: ${error}</p><p>You can close this window.</p></body></html>`,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (code && state) {\n\t\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\t`<html><body><h1>Authentication Successful</h1><p>You can close this window and return to the terminal.</p></body></html>`,\n\t\t\t\t\t);\n\t\t\t\t\tresult = { code, state };\n\t\t\t\t} else {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\t`<html><body><h1>Authentication Failed</h1><p>Missing code or state parameter.</p></body></html>`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tres.writeHead(404);\n\t\t\t\tres.end();\n\t\t\t}\n\t\t});\n\n\t\tserver.on(\"error\", (err) => {\n\t\t\treject(err);\n\t\t});\n\n\t\tserver.listen(51121, \"127.0.0.1\", () => {\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tcancelWait: () => {\n\t\t\t\t\tcancelled = true;\n\t\t\t\t},\n\t\t\t\twaitForCode: async () => {\n\t\t\t\t\tconst sleep = () => new Promise((r) => setTimeout(r, 100));\n\t\t\t\t\twhile (!result && !cancelled) {\n\t\t\t\t\t\tawait sleep();\n\t\t\t\t\t}\n\t\t\t\t\treturn result;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Parse redirect URL to extract code and state\n */\nfunction parseRedirectUrl(input: string): { code?: string; state?: string } {\n\tconst value = input.trim();\n\tif (!value) return {};\n\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn {\n\t\t\tcode: url.searchParams.get(\"code\") ?? undefined,\n\t\t\tstate: url.searchParams.get(\"state\") ?? undefined,\n\t\t};\n\t} catch {\n\t\t// Not a URL, return empty\n\t\treturn {};\n\t}\n}\n\ninterface LoadCodeAssistPayload {\n\tcloudaicompanionProject?: string | { id?: string };\n\tcurrentTier?: { id?: string };\n\tallowedTiers?: Array<{ id?: string; isDefault?: boolean }>;\n}\n\n/**\n * Discover or provision a project for the user\n */\nasync function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {\n\tconst headers = {\n\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\"Content-Type\": \"application/json\",\n\t\t\"User-Agent\": \"google-api-nodejs-client/9.15.1\",\n\t\t\"X-Goog-Api-Client\": \"google-cloud-sdk vscode_cloudshelleditor/0.1\",\n\t\t\"Client-Metadata\": JSON.stringify({\n\t\t\tideType: \"IDE_UNSPECIFIED\",\n\t\t\tplatform: \"PLATFORM_UNSPECIFIED\",\n\t\t\tpluginType: \"GEMINI\",\n\t\t}),\n\t};\n\n\t// Try endpoints in order: prod first, then sandbox\n\tconst endpoints = [\"https://cloudcode-pa.googleapis.com\", \"https://daily-cloudcode-pa.sandbox.googleapis.com\"];\n\n\tonProgress?.(\"Checking for existing project...\");\n\n\tfor (const endpoint of endpoints) {\n\t\ttry {\n\t\t\tconst loadResponse = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders,\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tideType: \"IDE_UNSPECIFIED\",\n\t\t\t\t\t\tplatform: \"PLATFORM_UNSPECIFIED\",\n\t\t\t\t\t\tpluginType: \"GEMINI\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (loadResponse.ok) {\n\t\t\t\tconst data = (await loadResponse.json()) as LoadCodeAssistPayload;\n\n\t\t\t\t// Handle both string and object formats\n\t\t\t\tif (typeof data.cloudaicompanionProject === \"string\" && data.cloudaicompanionProject) {\n\t\t\t\t\treturn data.cloudaicompanionProject;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tdata.cloudaicompanionProject &&\n\t\t\t\t\ttypeof data.cloudaicompanionProject === \"object\" &&\n\t\t\t\t\tdata.cloudaicompanionProject.id\n\t\t\t\t) {\n\t\t\t\t\treturn data.cloudaicompanionProject.id;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try next endpoint\n\t\t}\n\t}\n\n\t// Use fallback project ID\n\tonProgress?.(\"Using default project...\");\n\treturn DEFAULT_PROJECT_ID;\n}\n\n/**\n * Get user email from the access token\n */\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\n/**\n * Refresh Antigravity token\n */\nexport async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tclient_secret: CLIENT_SECRET,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Antigravity token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n\n/**\n * Login with Antigravity OAuth\n *\n * @param onAuth - Callback with URL and optional instructions\n * @param onProgress - Optional progress callback\n * @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.\n * Races with browser callback - whichever completes first wins.\n */\nexport async function loginAntigravity(\n\tonAuth: (info: { url: string; instructions?: string }) => void,\n\tonProgress?: (message: string) => void,\n\tonManualCodeInput?: () => Promise<string>,\n): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\n\t// Start local server for callback\n\tonProgress?.(\"Starting local server for OAuth callback...\");\n\tconst server = await startCallbackServer();\n\n\tlet code: string | undefined;\n\n\ttry {\n\t\t// Build authorization URL\n\t\tconst authParams = new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tresponse_type: \"code\",\n\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\tscope: SCOPES.join(\" \"),\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t\tstate: verifier,\n\t\t\taccess_type: \"offline\",\n\t\t\tprompt: \"consent\",\n\t\t});\n\n\t\tconst authUrl = `${AUTH_URL}?${authParams.toString()}`;\n\n\t\t// Notify caller with URL to open\n\t\tonAuth({\n\t\t\turl: authUrl,\n\t\t\tinstructions: \"Complete the sign-in in your browser.\",\n\t\t});\n\n\t\t// Wait for the callback, racing with manual input if provided\n\t\tonProgress?.(\"Waiting for OAuth callback...\");\n\n\t\tif (onManualCodeInput) {\n\t\t\t// Race between browser callback and manual input\n\t\t\tlet manualInput: string | undefined;\n\t\t\tlet manualError: Error | undefined;\n\t\t\tconst manualPromise = onManualCodeInput()\n\t\t\t\t.then((input) => {\n\t\t\t\t\tmanualInput = input;\n\t\t\t\t\tserver.cancelWait();\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tmanualError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\t\tserver.cancelWait();\n\t\t\t\t});\n\n\t\t\tconst result = await server.waitForCode();\n\n\t\t\t// If manual input was cancelled, throw that error\n\t\t\tif (manualError) {\n\t\t\t\tthrow manualError;\n\t\t\t}\n\n\t\t\tif (result?.code) {\n\t\t\t\t// Browser callback won - verify state\n\t\t\t\tif (result.state !== verifier) {\n\t\t\t\t\tthrow new Error(\"OAuth state mismatch - possible CSRF attack\");\n\t\t\t\t}\n\t\t\t\tcode = result.code;\n\t\t\t} else if (manualInput) {\n\t\t\t\t// Manual input won\n\t\t\t\tconst parsed = parseRedirectUrl(manualInput);\n\t\t\t\tif (parsed.state && parsed.state !== verifier) {\n\t\t\t\t\tthrow new Error(\"OAuth state mismatch - possible CSRF attack\");\n\t\t\t\t}\n\t\t\t\tcode = parsed.code;\n\t\t\t}\n\n\t\t\t// If still no code, wait for manual promise and try that\n\t\t\tif (!code) {\n\t\t\t\tawait manualPromise;\n\t\t\t\tif (manualError) {\n\t\t\t\t\tthrow manualError;\n\t\t\t\t}\n\t\t\t\tif (manualInput) {\n\t\t\t\t\tconst parsed = parseRedirectUrl(manualInput);\n\t\t\t\t\tif (parsed.state && parsed.state !== verifier) {\n\t\t\t\t\t\tthrow new Error(\"OAuth state mismatch - possible CSRF attack\");\n\t\t\t\t\t}\n\t\t\t\t\tcode = parsed.code;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Original flow: just wait for callback\n\t\t\tconst result = await server.waitForCode();\n\t\t\tif (result?.code) {\n\t\t\t\tif (result.state !== verifier) {\n\t\t\t\t\tthrow new Error(\"OAuth state mismatch - possible CSRF attack\");\n\t\t\t\t}\n\t\t\t\tcode = result.code;\n\t\t\t}\n\t\t}\n\n\t\tif (!code) {\n\t\t\tthrow new Error(\"No authorization code received\");\n\t\t}\n\n\t\t// Exchange code for tokens\n\t\tonProgress?.(\"Exchanging authorization code for tokens...\");\n\t\tconst tokenResponse = await fetch(TOKEN_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\tclient_secret: CLIENT_SECRET,\n\t\t\t\tcode,\n\t\t\t\tgrant_type: \"authorization_code\",\n\t\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\t\tcode_verifier: verifier,\n\t\t\t}),\n\t\t});\n\n\t\tif (!tokenResponse.ok) {\n\t\t\tconst error = await tokenResponse.text();\n\t\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t\t}\n\n\t\tconst tokenData = (await tokenResponse.json()) as {\n\t\t\taccess_token: string;\n\t\t\trefresh_token: string;\n\t\t\texpires_in: number;\n\t\t};\n\n\t\tif (!tokenData.refresh_token) {\n\t\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t\t}\n\n\t\t// Get user email\n\t\tonProgress?.(\"Getting user info...\");\n\t\tconst email = await getUserEmail(tokenData.access_token);\n\n\t\t// Discover project\n\t\tconst projectId = await discoverProject(tokenData.access_token, onProgress);\n\n\t\t// Calculate expiry time (current time + expires_in seconds - 5 min buffer)\n\t\tconst expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;\n\n\t\tconst credentials: OAuthCredentials = {\n\t\t\trefresh: tokenData.refresh_token,\n\t\t\taccess: tokenData.access_token,\n\t\t\texpires: expiresAt,\n\t\t\tprojectId,\n\t\t\temail,\n\t\t};\n\n\t\treturn credentials;\n\t} finally {\n\t\tserver.server.close();\n\t}\n}\n\nexport const antigravityOAuthProvider: OAuthProviderInterface = {\n\tid: \"google-antigravity\",\n\tname: \"Antigravity (Gemini 3, Claude, GPT-OSS)\",\n\tusesCallbackServer: true,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst creds = credentials as AntigravityCredentials;\n\t\tif (!creds.projectId) {\n\t\t\tthrow new Error(\"Antigravity credentials missing projectId\");\n\t\t}\n\t\treturn refreshAntigravityToken(creds.refresh, creds.projectId);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\tconst creds = credentials as AntigravityCredentials;\n\t\treturn JSON.stringify({ token: creds.access, projectId: creds.projectId });\n\t},\n};\n"]}
|