@lenylvt/pi-ai 0.64.0

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.
Files changed (167) hide show
  1. package/README.md +203 -0
  2. package/dist/api-registry.d.ts +20 -0
  3. package/dist/api-registry.d.ts.map +1 -0
  4. package/dist/api-registry.js +44 -0
  5. package/dist/api-registry.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +119 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/env-api-keys.d.ts +7 -0
  11. package/dist/env-api-keys.d.ts.map +1 -0
  12. package/dist/env-api-keys.js +13 -0
  13. package/dist/env-api-keys.js.map +1 -0
  14. package/dist/index.d.ts +20 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +14 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/models.d.ts +24 -0
  19. package/dist/models.d.ts.map +1 -0
  20. package/dist/models.generated.d.ts +2332 -0
  21. package/dist/models.generated.d.ts.map +1 -0
  22. package/dist/models.generated.js +2186 -0
  23. package/dist/models.generated.js.map +1 -0
  24. package/dist/models.js +60 -0
  25. package/dist/models.js.map +1 -0
  26. package/dist/oauth.d.ts +2 -0
  27. package/dist/oauth.d.ts.map +1 -0
  28. package/dist/oauth.js +2 -0
  29. package/dist/oauth.js.map +1 -0
  30. package/dist/providers/anthropic.d.ts +40 -0
  31. package/dist/providers/anthropic.d.ts.map +1 -0
  32. package/dist/providers/anthropic.js +749 -0
  33. package/dist/providers/anthropic.js.map +1 -0
  34. package/dist/providers/faux.d.ts +56 -0
  35. package/dist/providers/faux.d.ts.map +1 -0
  36. package/dist/providers/faux.js +367 -0
  37. package/dist/providers/faux.js.map +1 -0
  38. package/dist/providers/github-copilot-headers.d.ts +8 -0
  39. package/dist/providers/github-copilot-headers.d.ts.map +1 -0
  40. package/dist/providers/github-copilot-headers.js +29 -0
  41. package/dist/providers/github-copilot-headers.js.map +1 -0
  42. package/dist/providers/openai-codex-responses.d.ts +9 -0
  43. package/dist/providers/openai-codex-responses.d.ts.map +1 -0
  44. package/dist/providers/openai-codex-responses.js +741 -0
  45. package/dist/providers/openai-codex-responses.js.map +1 -0
  46. package/dist/providers/openai-completions.d.ts +15 -0
  47. package/dist/providers/openai-completions.d.ts.map +1 -0
  48. package/dist/providers/openai-completions.js +687 -0
  49. package/dist/providers/openai-completions.js.map +1 -0
  50. package/dist/providers/openai-responses-shared.d.ts +17 -0
  51. package/dist/providers/openai-responses-shared.d.ts.map +1 -0
  52. package/dist/providers/openai-responses-shared.js +458 -0
  53. package/dist/providers/openai-responses-shared.js.map +1 -0
  54. package/dist/providers/openai-responses.d.ts +13 -0
  55. package/dist/providers/openai-responses.d.ts.map +1 -0
  56. package/dist/providers/openai-responses.js +190 -0
  57. package/dist/providers/openai-responses.js.map +1 -0
  58. package/dist/providers/register-builtins.d.ts +16 -0
  59. package/dist/providers/register-builtins.d.ts.map +1 -0
  60. package/dist/providers/register-builtins.js +140 -0
  61. package/dist/providers/register-builtins.js.map +1 -0
  62. package/dist/providers/simple-options.d.ts +8 -0
  63. package/dist/providers/simple-options.d.ts.map +1 -0
  64. package/dist/providers/simple-options.js +35 -0
  65. package/dist/providers/simple-options.js.map +1 -0
  66. package/dist/providers/transform-messages.d.ts +8 -0
  67. package/dist/providers/transform-messages.d.ts.map +1 -0
  68. package/dist/providers/transform-messages.js +155 -0
  69. package/dist/providers/transform-messages.js.map +1 -0
  70. package/dist/stream.d.ts +8 -0
  71. package/dist/stream.d.ts.map +1 -0
  72. package/dist/stream.js +27 -0
  73. package/dist/stream.js.map +1 -0
  74. package/dist/types.d.ts +283 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +2 -0
  77. package/dist/types.js.map +1 -0
  78. package/dist/utils/event-stream.d.ts +21 -0
  79. package/dist/utils/event-stream.d.ts.map +1 -0
  80. package/dist/utils/event-stream.js +81 -0
  81. package/dist/utils/event-stream.js.map +1 -0
  82. package/dist/utils/hash.d.ts +3 -0
  83. package/dist/utils/hash.d.ts.map +1 -0
  84. package/dist/utils/hash.js +14 -0
  85. package/dist/utils/hash.js.map +1 -0
  86. package/dist/utils/json-parse.d.ts +9 -0
  87. package/dist/utils/json-parse.d.ts.map +1 -0
  88. package/dist/utils/json-parse.js +29 -0
  89. package/dist/utils/json-parse.js.map +1 -0
  90. package/dist/utils/oauth/anthropic.d.ts +25 -0
  91. package/dist/utils/oauth/anthropic.d.ts.map +1 -0
  92. package/dist/utils/oauth/anthropic.js +335 -0
  93. package/dist/utils/oauth/anthropic.js.map +1 -0
  94. package/dist/utils/oauth/github-copilot.d.ts +30 -0
  95. package/dist/utils/oauth/github-copilot.d.ts.map +1 -0
  96. package/dist/utils/oauth/github-copilot.js +292 -0
  97. package/dist/utils/oauth/github-copilot.js.map +1 -0
  98. package/dist/utils/oauth/index.d.ts +36 -0
  99. package/dist/utils/oauth/index.d.ts.map +1 -0
  100. package/dist/utils/oauth/index.js +92 -0
  101. package/dist/utils/oauth/index.js.map +1 -0
  102. package/dist/utils/oauth/oauth-page.d.ts +3 -0
  103. package/dist/utils/oauth/oauth-page.d.ts.map +1 -0
  104. package/dist/utils/oauth/oauth-page.js +105 -0
  105. package/dist/utils/oauth/oauth-page.js.map +1 -0
  106. package/dist/utils/oauth/openai-codex.d.ts +34 -0
  107. package/dist/utils/oauth/openai-codex.d.ts.map +1 -0
  108. package/dist/utils/oauth/openai-codex.js +373 -0
  109. package/dist/utils/oauth/openai-codex.js.map +1 -0
  110. package/dist/utils/oauth/pkce.d.ts +13 -0
  111. package/dist/utils/oauth/pkce.d.ts.map +1 -0
  112. package/dist/utils/oauth/pkce.js +31 -0
  113. package/dist/utils/oauth/pkce.js.map +1 -0
  114. package/dist/utils/oauth/types.d.ts +47 -0
  115. package/dist/utils/oauth/types.d.ts.map +1 -0
  116. package/dist/utils/oauth/types.js +2 -0
  117. package/dist/utils/oauth/types.js.map +1 -0
  118. package/dist/utils/overflow.d.ts +53 -0
  119. package/dist/utils/overflow.d.ts.map +1 -0
  120. package/dist/utils/overflow.js +119 -0
  121. package/dist/utils/overflow.js.map +1 -0
  122. package/dist/utils/sanitize-unicode.d.ts +22 -0
  123. package/dist/utils/sanitize-unicode.d.ts.map +1 -0
  124. package/dist/utils/sanitize-unicode.js +26 -0
  125. package/dist/utils/sanitize-unicode.js.map +1 -0
  126. package/dist/utils/typebox-helpers.d.ts +17 -0
  127. package/dist/utils/typebox-helpers.d.ts.map +1 -0
  128. package/dist/utils/typebox-helpers.js +21 -0
  129. package/dist/utils/typebox-helpers.js.map +1 -0
  130. package/dist/utils/validation.d.ts +18 -0
  131. package/dist/utils/validation.d.ts.map +1 -0
  132. package/dist/utils/validation.js +80 -0
  133. package/dist/utils/validation.js.map +1 -0
  134. package/package.json +89 -0
  135. package/src/api-registry.ts +98 -0
  136. package/src/cli.ts +136 -0
  137. package/src/env-api-keys.ts +22 -0
  138. package/src/index.ts +29 -0
  139. package/src/models.generated.ts +2188 -0
  140. package/src/models.ts +82 -0
  141. package/src/oauth.ts +1 -0
  142. package/src/providers/anthropic.ts +905 -0
  143. package/src/providers/faux.ts +498 -0
  144. package/src/providers/github-copilot-headers.ts +37 -0
  145. package/src/providers/openai-codex-responses.ts +929 -0
  146. package/src/providers/openai-completions.ts +811 -0
  147. package/src/providers/openai-responses-shared.ts +513 -0
  148. package/src/providers/openai-responses.ts +251 -0
  149. package/src/providers/register-builtins.ts +232 -0
  150. package/src/providers/simple-options.ts +46 -0
  151. package/src/providers/transform-messages.ts +172 -0
  152. package/src/stream.ts +59 -0
  153. package/src/types.ts +294 -0
  154. package/src/utils/event-stream.ts +87 -0
  155. package/src/utils/hash.ts +13 -0
  156. package/src/utils/json-parse.ts +28 -0
  157. package/src/utils/oauth/anthropic.ts +402 -0
  158. package/src/utils/oauth/github-copilot.ts +396 -0
  159. package/src/utils/oauth/index.ts +123 -0
  160. package/src/utils/oauth/oauth-page.ts +109 -0
  161. package/src/utils/oauth/openai-codex.ts +450 -0
  162. package/src/utils/oauth/pkce.ts +34 -0
  163. package/src/utils/oauth/types.ts +59 -0
  164. package/src/utils/overflow.ts +125 -0
  165. package/src/utils/sanitize-unicode.ts +25 -0
  166. package/src/utils/typebox-helpers.ts +24 -0
  167. package/src/utils/validation.ts +93 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * GitHub Copilot OAuth flow
3
+ */
4
+
5
+ import { getModels } from "../../models.js";
6
+ import type { Api, Model } from "../../types.js";
7
+ import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js";
8
+
9
+ type CopilotCredentials = OAuthCredentials & {
10
+ enterpriseUrl?: string;
11
+ };
12
+
13
+ const decode = (s: string) => atob(s);
14
+ const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
15
+
16
+ const COPILOT_HEADERS = {
17
+ "User-Agent": "GitHubCopilotChat/0.35.0",
18
+ "Editor-Version": "vscode/1.107.0",
19
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
20
+ "Copilot-Integration-Id": "vscode-chat",
21
+ } as const;
22
+
23
+ const INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;
24
+ const SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4;
25
+
26
+ type DeviceCodeResponse = {
27
+ device_code: string;
28
+ user_code: string;
29
+ verification_uri: string;
30
+ interval: number;
31
+ expires_in: number;
32
+ };
33
+
34
+ type DeviceTokenSuccessResponse = {
35
+ access_token: string;
36
+ token_type?: string;
37
+ scope?: string;
38
+ };
39
+
40
+ type DeviceTokenErrorResponse = {
41
+ error: string;
42
+ error_description?: string;
43
+ interval?: number;
44
+ };
45
+
46
+ export function normalizeDomain(input: string): string | null {
47
+ const trimmed = input.trim();
48
+ if (!trimmed) return null;
49
+ try {
50
+ const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
51
+ return url.hostname;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function getUrls(domain: string): {
58
+ deviceCodeUrl: string;
59
+ accessTokenUrl: string;
60
+ copilotTokenUrl: string;
61
+ } {
62
+ return {
63
+ deviceCodeUrl: `https://${domain}/login/device/code`,
64
+ accessTokenUrl: `https://${domain}/login/oauth/access_token`,
65
+ copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Parse the proxy-ep from a Copilot token and convert to API base URL.
71
+ * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
72
+ * Returns API URL like https://api.individual.githubcopilot.com
73
+ */
74
+ function getBaseUrlFromToken(token: string): string | null {
75
+ const match = token.match(/proxy-ep=([^;]+)/);
76
+ if (!match) return null;
77
+ const proxyHost = match[1];
78
+ // Convert proxy.xxx to api.xxx
79
+ const apiHost = proxyHost.replace(/^proxy\./, "api.");
80
+ return `https://${apiHost}`;
81
+ }
82
+
83
+ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
84
+ // If we have a token, extract the base URL from proxy-ep
85
+ if (token) {
86
+ const urlFromToken = getBaseUrlFromToken(token);
87
+ if (urlFromToken) return urlFromToken;
88
+ }
89
+ // Fallback for enterprise or if token parsing fails
90
+ if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;
91
+ return "https://api.individual.githubcopilot.com";
92
+ }
93
+
94
+ async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
95
+ const response = await fetch(url, init);
96
+ if (!response.ok) {
97
+ const text = await response.text();
98
+ throw new Error(`${response.status} ${response.statusText}: ${text}`);
99
+ }
100
+ return response.json();
101
+ }
102
+
103
+ async function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {
104
+ const urls = getUrls(domain);
105
+ const data = await fetchJson(urls.deviceCodeUrl, {
106
+ method: "POST",
107
+ headers: {
108
+ Accept: "application/json",
109
+ "Content-Type": "application/x-www-form-urlencoded",
110
+ "User-Agent": "GitHubCopilotChat/0.35.0",
111
+ },
112
+ body: new URLSearchParams({
113
+ client_id: CLIENT_ID,
114
+ scope: "read:user",
115
+ }),
116
+ });
117
+
118
+ if (!data || typeof data !== "object") {
119
+ throw new Error("Invalid device code response");
120
+ }
121
+
122
+ const deviceCode = (data as Record<string, unknown>).device_code;
123
+ const userCode = (data as Record<string, unknown>).user_code;
124
+ const verificationUri = (data as Record<string, unknown>).verification_uri;
125
+ const interval = (data as Record<string, unknown>).interval;
126
+ const expiresIn = (data as Record<string, unknown>).expires_in;
127
+
128
+ if (
129
+ typeof deviceCode !== "string" ||
130
+ typeof userCode !== "string" ||
131
+ typeof verificationUri !== "string" ||
132
+ typeof interval !== "number" ||
133
+ typeof expiresIn !== "number"
134
+ ) {
135
+ throw new Error("Invalid device code response fields");
136
+ }
137
+
138
+ return {
139
+ device_code: deviceCode,
140
+ user_code: userCode,
141
+ verification_uri: verificationUri,
142
+ interval,
143
+ expires_in: expiresIn,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Sleep that can be interrupted by an AbortSignal
149
+ */
150
+ function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
151
+ return new Promise((resolve, reject) => {
152
+ if (signal?.aborted) {
153
+ reject(new Error("Login cancelled"));
154
+ return;
155
+ }
156
+
157
+ const timeout = setTimeout(resolve, ms);
158
+
159
+ signal?.addEventListener(
160
+ "abort",
161
+ () => {
162
+ clearTimeout(timeout);
163
+ reject(new Error("Login cancelled"));
164
+ },
165
+ { once: true },
166
+ );
167
+ });
168
+ }
169
+
170
+ async function pollForGitHubAccessToken(
171
+ domain: string,
172
+ deviceCode: string,
173
+ intervalSeconds: number,
174
+ expiresIn: number,
175
+ signal?: AbortSignal,
176
+ ) {
177
+ const urls = getUrls(domain);
178
+ const deadline = Date.now() + expiresIn * 1000;
179
+ let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
180
+ let intervalMultiplier = INITIAL_POLL_INTERVAL_MULTIPLIER;
181
+ let slowDownResponses = 0;
182
+
183
+ while (Date.now() < deadline) {
184
+ if (signal?.aborted) {
185
+ throw new Error("Login cancelled");
186
+ }
187
+
188
+ const remainingMs = deadline - Date.now();
189
+ const waitMs = Math.min(Math.ceil(intervalMs * intervalMultiplier), remainingMs);
190
+ await abortableSleep(waitMs, signal);
191
+
192
+ const raw = await fetchJson(urls.accessTokenUrl, {
193
+ method: "POST",
194
+ headers: {
195
+ Accept: "application/json",
196
+ "Content-Type": "application/x-www-form-urlencoded",
197
+ "User-Agent": "GitHubCopilotChat/0.35.0",
198
+ },
199
+ body: new URLSearchParams({
200
+ client_id: CLIENT_ID,
201
+ device_code: deviceCode,
202
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
203
+ }),
204
+ });
205
+
206
+ if (raw && typeof raw === "object" && typeof (raw as DeviceTokenSuccessResponse).access_token === "string") {
207
+ return (raw as DeviceTokenSuccessResponse).access_token;
208
+ }
209
+
210
+ if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") {
211
+ const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;
212
+ if (error === "authorization_pending") {
213
+ continue;
214
+ }
215
+
216
+ if (error === "slow_down") {
217
+ slowDownResponses += 1;
218
+ intervalMs =
219
+ typeof interval === "number" && interval > 0 ? interval * 1000 : Math.max(1000, intervalMs + 5000);
220
+ intervalMultiplier = SLOW_DOWN_POLL_INTERVAL_MULTIPLIER;
221
+ continue;
222
+ }
223
+
224
+ const descriptionSuffix = description ? `: ${description}` : "";
225
+ throw new Error(`Device flow failed: ${error}${descriptionSuffix}`);
226
+ }
227
+ }
228
+
229
+ if (slowDownResponses > 0) {
230
+ throw new Error(
231
+ "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.",
232
+ );
233
+ }
234
+
235
+ throw new Error("Device flow timed out");
236
+ }
237
+
238
+ /**
239
+ * Refresh GitHub Copilot token
240
+ */
241
+ export async function refreshGitHubCopilotToken(
242
+ refreshToken: string,
243
+ enterpriseDomain?: string,
244
+ ): Promise<OAuthCredentials> {
245
+ const domain = enterpriseDomain || "github.com";
246
+ const urls = getUrls(domain);
247
+
248
+ const raw = await fetchJson(urls.copilotTokenUrl, {
249
+ headers: {
250
+ Accept: "application/json",
251
+ Authorization: `Bearer ${refreshToken}`,
252
+ ...COPILOT_HEADERS,
253
+ },
254
+ });
255
+
256
+ if (!raw || typeof raw !== "object") {
257
+ throw new Error("Invalid Copilot token response");
258
+ }
259
+
260
+ const token = (raw as Record<string, unknown>).token;
261
+ const expiresAt = (raw as Record<string, unknown>).expires_at;
262
+
263
+ if (typeof token !== "string" || typeof expiresAt !== "number") {
264
+ throw new Error("Invalid Copilot token response fields");
265
+ }
266
+
267
+ return {
268
+ refresh: refreshToken,
269
+ access: token,
270
+ expires: expiresAt * 1000 - 5 * 60 * 1000,
271
+ enterpriseUrl: enterpriseDomain,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Enable a model for the user's GitHub Copilot account.
277
+ * This is required for some models (like Claude, Grok) before they can be used.
278
+ */
279
+ async function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {
280
+ const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
281
+ const url = `${baseUrl}/models/${modelId}/policy`;
282
+
283
+ try {
284
+ const response = await fetch(url, {
285
+ method: "POST",
286
+ headers: {
287
+ "Content-Type": "application/json",
288
+ Authorization: `Bearer ${token}`,
289
+ ...COPILOT_HEADERS,
290
+ "openai-intent": "chat-policy",
291
+ "x-interaction-type": "chat-policy",
292
+ },
293
+ body: JSON.stringify({ state: "enabled" }),
294
+ });
295
+ return response.ok;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Enable all known GitHub Copilot models that may require policy acceptance.
303
+ * Called after successful login to ensure all models are available.
304
+ */
305
+ async function enableAllGitHubCopilotModels(
306
+ token: string,
307
+ enterpriseDomain?: string,
308
+ onProgress?: (model: string, success: boolean) => void,
309
+ ): Promise<void> {
310
+ const models = getModels("github-copilot");
311
+ await Promise.all(
312
+ models.map(async (model) => {
313
+ const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
314
+ onProgress?.(model.id, success);
315
+ }),
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Login with GitHub Copilot OAuth (device code flow)
321
+ *
322
+ * @param options.onAuth - Callback with URL and optional instructions (user code)
323
+ * @param options.onPrompt - Callback to prompt user for input
324
+ * @param options.onProgress - Optional progress callback
325
+ * @param options.signal - Optional AbortSignal for cancellation
326
+ */
327
+ export async function loginGitHubCopilot(options: {
328
+ onAuth: (url: string, instructions?: string) => void;
329
+ onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;
330
+ onProgress?: (message: string) => void;
331
+ signal?: AbortSignal;
332
+ }): Promise<OAuthCredentials> {
333
+ const input = await options.onPrompt({
334
+ message: "GitHub Enterprise URL/domain (blank for github.com)",
335
+ placeholder: "company.ghe.com",
336
+ allowEmpty: true,
337
+ });
338
+
339
+ if (options.signal?.aborted) {
340
+ throw new Error("Login cancelled");
341
+ }
342
+
343
+ const trimmed = input.trim();
344
+ const enterpriseDomain = normalizeDomain(input);
345
+ if (trimmed && !enterpriseDomain) {
346
+ throw new Error("Invalid GitHub Enterprise URL/domain");
347
+ }
348
+ const domain = enterpriseDomain || "github.com";
349
+
350
+ const device = await startDeviceFlow(domain);
351
+ options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
352
+
353
+ const githubAccessToken = await pollForGitHubAccessToken(
354
+ domain,
355
+ device.device_code,
356
+ device.interval,
357
+ device.expires_in,
358
+ options.signal,
359
+ );
360
+ const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
361
+
362
+ // Enable all models after successful login
363
+ options.onProgress?.("Enabling models...");
364
+ await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
365
+ return credentials;
366
+ }
367
+
368
+ export const githubCopilotOAuthProvider: OAuthProviderInterface = {
369
+ id: "github-copilot",
370
+ name: "GitHub Copilot",
371
+
372
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
373
+ return loginGitHubCopilot({
374
+ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
375
+ onPrompt: callbacks.onPrompt,
376
+ onProgress: callbacks.onProgress,
377
+ signal: callbacks.signal,
378
+ });
379
+ },
380
+
381
+ async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
382
+ const creds = credentials as CopilotCredentials;
383
+ return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);
384
+ },
385
+
386
+ getApiKey(credentials: OAuthCredentials): string {
387
+ return credentials.access;
388
+ },
389
+
390
+ modifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {
391
+ const creds = credentials as CopilotCredentials;
392
+ const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;
393
+ const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);
394
+ return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
395
+ },
396
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * OAuth credential management for the supported built-in providers.
3
+ *
4
+ * Supported OAuth providers:
5
+ * - Anthropic (Claude Pro/Max)
6
+ * - GitHub Copilot
7
+ * - OpenAI Codex (ChatGPT Plus/Pro)
8
+ */
9
+
10
+ export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
11
+ export {
12
+ getGitHubCopilotBaseUrl,
13
+ githubCopilotOAuthProvider,
14
+ loginGitHubCopilot,
15
+ normalizeDomain,
16
+ refreshGitHubCopilotToken,
17
+ } from "./github-copilot.js";
18
+ export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js";
19
+
20
+ export * from "./types.js";
21
+
22
+ import { anthropicOAuthProvider } from "./anthropic.js";
23
+ import { githubCopilotOAuthProvider } from "./github-copilot.js";
24
+ import { openaiCodexOAuthProvider } from "./openai-codex.js";
25
+ import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js";
26
+
27
+ const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
28
+ anthropicOAuthProvider,
29
+ githubCopilotOAuthProvider,
30
+ openaiCodexOAuthProvider,
31
+ ];
32
+
33
+ const oauthProviderRegistry = new Map<string, OAuthProviderInterface>(
34
+ BUILT_IN_OAUTH_PROVIDERS.map((provider) => [provider.id, provider]),
35
+ );
36
+
37
+ export function getOAuthProvider(id: OAuthProviderId): OAuthProviderInterface | undefined {
38
+ return oauthProviderRegistry.get(id);
39
+ }
40
+
41
+ export function registerOAuthProvider(provider: OAuthProviderInterface): void {
42
+ oauthProviderRegistry.set(provider.id, provider);
43
+ }
44
+
45
+ export function unregisterOAuthProvider(id: string): void {
46
+ const builtInProvider = BUILT_IN_OAUTH_PROVIDERS.find((provider) => provider.id === id);
47
+ if (builtInProvider) {
48
+ oauthProviderRegistry.set(id, builtInProvider);
49
+ return;
50
+ }
51
+
52
+ oauthProviderRegistry.delete(id);
53
+ }
54
+
55
+ export function resetOAuthProviders(): void {
56
+ oauthProviderRegistry.clear();
57
+ for (const provider of BUILT_IN_OAUTH_PROVIDERS) {
58
+ oauthProviderRegistry.set(provider.id, provider);
59
+ }
60
+ }
61
+
62
+ export function getOAuthProviders(): OAuthProviderInterface[] {
63
+ return Array.from(oauthProviderRegistry.values());
64
+ }
65
+
66
+ /**
67
+ * @deprecated Use getOAuthProviders() which returns OAuthProviderInterface[]
68
+ */
69
+ export function getOAuthProviderInfoList(): OAuthProviderInfo[] {
70
+ return getOAuthProviders().map((provider) => ({
71
+ id: provider.id,
72
+ name: provider.name,
73
+ available: true,
74
+ }));
75
+ }
76
+
77
+ /**
78
+ * Refresh token for any OAuth provider.
79
+ * @deprecated Use getOAuthProvider(id).refreshToken() instead
80
+ */
81
+ export async function refreshOAuthToken(
82
+ providerId: OAuthProviderId,
83
+ credentials: OAuthCredentials,
84
+ ): Promise<OAuthCredentials> {
85
+ const provider = getOAuthProvider(providerId);
86
+ if (!provider) {
87
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
88
+ }
89
+
90
+ return provider.refreshToken(credentials);
91
+ }
92
+
93
+ /**
94
+ * Get API key for a provider from OAuth credentials.
95
+ * Automatically refreshes expired tokens.
96
+ */
97
+ export async function getOAuthApiKey(
98
+ providerId: OAuthProviderId,
99
+ credentials: Record<string, OAuthCredentials>,
100
+ ): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> {
101
+ const provider = getOAuthProvider(providerId);
102
+ if (!provider) {
103
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
104
+ }
105
+
106
+ let creds = credentials[providerId];
107
+ if (!creds) {
108
+ return null;
109
+ }
110
+
111
+ if (Date.now() >= creds.expires) {
112
+ try {
113
+ creds = await provider.refreshToken(creds);
114
+ } catch {
115
+ throw new Error(`Failed to refresh OAuth token for ${providerId}`);
116
+ }
117
+ }
118
+
119
+ return {
120
+ newCredentials: creds,
121
+ apiKey: provider.getApiKey(creds),
122
+ };
123
+ }
@@ -0,0 +1,109 @@
1
+ const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;
2
+
3
+ function escapeHtml(value: string): string {
4
+ return value
5
+ .replaceAll("&", "&amp;")
6
+ .replaceAll("<", "&lt;")
7
+ .replaceAll(">", "&gt;")
8
+ .replaceAll('"', "&quot;")
9
+ .replaceAll("'", "&#39;");
10
+ }
11
+
12
+ function renderPage(options: { title: string; heading: string; message: string; details?: string }): string {
13
+ const title = escapeHtml(options.title);
14
+ const heading = escapeHtml(options.heading);
15
+ const message = escapeHtml(options.message);
16
+ const details = options.details ? escapeHtml(options.details) : undefined;
17
+
18
+ return `<!doctype html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="utf-8" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <title>${title}</title>
24
+ <style>
25
+ :root {
26
+ --text: #fafafa;
27
+ --text-dim: #a1a1aa;
28
+ --page-bg: #09090b;
29
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
30
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
31
+ }
32
+ * { box-sizing: border-box; }
33
+ html { color-scheme: dark; }
34
+ body {
35
+ margin: 0;
36
+ min-height: 100vh;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ padding: 24px;
41
+ background: var(--page-bg);
42
+ color: var(--text);
43
+ font-family: var(--font-sans);
44
+ text-align: center;
45
+ }
46
+ main {
47
+ width: 100%;
48
+ max-width: 560px;
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: center;
52
+ justify-content: center;
53
+ }
54
+ .logo {
55
+ width: 72px;
56
+ height: 72px;
57
+ display: block;
58
+ margin-bottom: 24px;
59
+ }
60
+ h1 {
61
+ margin: 0 0 10px;
62
+ font-size: 28px;
63
+ line-height: 1.15;
64
+ font-weight: 650;
65
+ color: var(--text);
66
+ }
67
+ p {
68
+ margin: 0;
69
+ line-height: 1.7;
70
+ color: var(--text-dim);
71
+ font-size: 15px;
72
+ }
73
+ .details {
74
+ margin-top: 16px;
75
+ font-family: var(--font-mono);
76
+ font-size: 13px;
77
+ color: var(--text-dim);
78
+ white-space: pre-wrap;
79
+ word-break: break-word;
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <main>
85
+ <div class="logo">${LOGO_SVG}</div>
86
+ <h1>${heading}</h1>
87
+ <p>${message}</p>
88
+ ${details ? `<div class="details">${details}</div>` : ""}
89
+ </main>
90
+ </body>
91
+ </html>`;
92
+ }
93
+
94
+ export function oauthSuccessHtml(message: string): string {
95
+ return renderPage({
96
+ title: "Authentication successful",
97
+ heading: "Authentication successful",
98
+ message,
99
+ });
100
+ }
101
+
102
+ export function oauthErrorHtml(message: string, details?: string): string {
103
+ return renderPage({
104
+ title: "Authentication failed",
105
+ heading: "Authentication failed",
106
+ message,
107
+ details,
108
+ });
109
+ }