copilot-router 1.0.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +32 -0
  5. package/dist/lib/api-config.d.ts +15 -0
  6. package/dist/lib/api-config.js +30 -0
  7. package/dist/lib/database.d.ts +60 -0
  8. package/dist/lib/database.js +228 -0
  9. package/dist/lib/error.d.ts +11 -0
  10. package/dist/lib/error.js +34 -0
  11. package/dist/lib/state.d.ts +9 -0
  12. package/dist/lib/state.js +3 -0
  13. package/dist/lib/token-manager.d.ts +95 -0
  14. package/dist/lib/token-manager.js +241 -0
  15. package/dist/lib/utils.d.ts +8 -0
  16. package/dist/lib/utils.js +10 -0
  17. package/dist/main.d.ts +1 -0
  18. package/dist/main.js +97 -0
  19. package/dist/routes/anthropic/routes.d.ts +2 -0
  20. package/dist/routes/anthropic/routes.js +155 -0
  21. package/dist/routes/anthropic/stream-translation.d.ts +3 -0
  22. package/dist/routes/anthropic/stream-translation.js +136 -0
  23. package/dist/routes/anthropic/translation.d.ts +4 -0
  24. package/dist/routes/anthropic/translation.js +241 -0
  25. package/dist/routes/anthropic/types.d.ts +165 -0
  26. package/dist/routes/anthropic/types.js +2 -0
  27. package/dist/routes/anthropic/utils.d.ts +2 -0
  28. package/dist/routes/anthropic/utils.js +12 -0
  29. package/dist/routes/auth/routes.d.ts +2 -0
  30. package/dist/routes/auth/routes.js +158 -0
  31. package/dist/routes/gemini/routes.d.ts +2 -0
  32. package/dist/routes/gemini/routes.js +163 -0
  33. package/dist/routes/gemini/translation.d.ts +5 -0
  34. package/dist/routes/gemini/translation.js +215 -0
  35. package/dist/routes/gemini/types.d.ts +63 -0
  36. package/dist/routes/gemini/types.js +2 -0
  37. package/dist/routes/openai/routes.d.ts +2 -0
  38. package/dist/routes/openai/routes.js +215 -0
  39. package/dist/routes/utility/routes.d.ts +2 -0
  40. package/dist/routes/utility/routes.js +28 -0
  41. package/dist/services/copilot/create-chat-completions.d.ts +130 -0
  42. package/dist/services/copilot/create-chat-completions.js +32 -0
  43. package/dist/services/copilot/create-embeddings.d.ts +20 -0
  44. package/dist/services/copilot/create-embeddings.js +19 -0
  45. package/dist/services/copilot/get-models.d.ts +51 -0
  46. package/dist/services/copilot/get-models.js +45 -0
  47. package/dist/services/github/get-device-code.d.ts +11 -0
  48. package/dist/services/github/get-device-code.js +21 -0
  49. package/dist/services/github/get-user.d.ts +11 -0
  50. package/dist/services/github/get-user.js +17 -0
  51. package/dist/services/github/poll-access-token.d.ts +13 -0
  52. package/dist/services/github/poll-access-token.js +56 -0
  53. package/package.json +56 -0
  54. package/public/index.html +419 -0
@@ -0,0 +1,215 @@
1
+ import { createRoute, z } from "@hono/zod-openapi";
2
+ import { streamSSE } from "hono/streaming";
3
+ import consola from "consola";
4
+ import { forwardError } from "../../lib/error.js";
5
+ import { tokenManager } from "../../lib/token-manager.js";
6
+ import { createChatCompletions, } from "../../services/copilot/create-chat-completions.js";
7
+ import { createEmbeddings, } from "../../services/copilot/create-embeddings.js";
8
+ import { getModels, getModelsForAllTokens } from "../../services/copilot/get-models.js";
9
+ const CommonResponseError = z.object({
10
+ error: z.object({
11
+ message: z.string(),
12
+ type: z.string(),
13
+ }),
14
+ });
15
+ // Chat completions route
16
+ const chatCompletionsRoute = createRoute({
17
+ method: "post",
18
+ path: "/chat/completions",
19
+ tags: ["OpenAI API"],
20
+ summary: "Create a chat completion",
21
+ description: "Create a chat completion using the OpenAI-compatible API interface.",
22
+ request: {
23
+ body: {
24
+ content: {
25
+ "application/json": {
26
+ schema: z.object({}).passthrough(),
27
+ },
28
+ },
29
+ },
30
+ },
31
+ responses: {
32
+ 200: {
33
+ content: {
34
+ "application/json": {
35
+ schema: z.object({}).passthrough(),
36
+ },
37
+ "text/event-stream": {
38
+ schema: z.string(),
39
+ },
40
+ },
41
+ description: "Successfully created chat completion",
42
+ },
43
+ 400: {
44
+ content: { "application/json": { schema: CommonResponseError } },
45
+ description: "Bad request",
46
+ },
47
+ 500: {
48
+ content: { "application/json": { schema: CommonResponseError } },
49
+ description: "Internal server error",
50
+ },
51
+ },
52
+ });
53
+ // Models route
54
+ const modelsRoute = createRoute({
55
+ method: "get",
56
+ path: "/models",
57
+ tags: ["OpenAI API"],
58
+ summary: "List available models",
59
+ description: "List all available models from GitHub Copilot.",
60
+ responses: {
61
+ 200: {
62
+ content: {
63
+ "application/json": {
64
+ schema: z.object({
65
+ object: z.string(),
66
+ data: z.array(z.object({}).passthrough()),
67
+ has_more: z.boolean(),
68
+ }),
69
+ },
70
+ },
71
+ description: "Successfully retrieved models",
72
+ },
73
+ 500: {
74
+ content: { "application/json": { schema: CommonResponseError } },
75
+ description: "Internal server error",
76
+ },
77
+ },
78
+ });
79
+ // Embeddings route
80
+ const embeddingsRoute = createRoute({
81
+ method: "post",
82
+ path: "/embeddings",
83
+ tags: ["OpenAI API"],
84
+ summary: "Create embeddings",
85
+ description: "Create embeddings for the provided input text.",
86
+ request: {
87
+ body: {
88
+ content: {
89
+ "application/json": {
90
+ schema: z.object({
91
+ input: z.union([z.string(), z.array(z.string())]),
92
+ model: z.string(),
93
+ }),
94
+ },
95
+ },
96
+ },
97
+ },
98
+ responses: {
99
+ 200: {
100
+ content: {
101
+ "application/json": {
102
+ schema: z.object({}).passthrough(),
103
+ },
104
+ },
105
+ description: "Successfully created embeddings",
106
+ },
107
+ 500: {
108
+ content: { "application/json": { schema: CommonResponseError } },
109
+ description: "Internal server error",
110
+ },
111
+ },
112
+ });
113
+ const isNonStreaming = (response) => Object.hasOwn(response, "choices");
114
+ export function registerOpenAIRoutes(app) {
115
+ // POST /chat/completions
116
+ app.openapi(chatCompletionsRoute, async (c) => {
117
+ try {
118
+ const payload = await c.req.json();
119
+ consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
120
+ // Get a random token entry for load balancing
121
+ const tokenEntry = tokenManager.getRandomTokenEntry();
122
+ if (!tokenEntry) {
123
+ return c.json({ error: { message: "No active tokens available", type: "auth_error" } }, 503);
124
+ }
125
+ // Fetch models if needed
126
+ if (!tokenEntry.models) {
127
+ try {
128
+ tokenEntry.models = await getModels(tokenEntry);
129
+ }
130
+ catch (e) {
131
+ consola.warn("Could not fetch models for max_tokens lookup");
132
+ }
133
+ }
134
+ const selectedModel = tokenEntry.models?.data.find((model) => model.id === payload.model);
135
+ if (!payload.max_tokens && selectedModel) {
136
+ payload.max_tokens = selectedModel.capabilities.limits.max_output_tokens;
137
+ }
138
+ const response = await createChatCompletions(payload, tokenEntry);
139
+ if (isNonStreaming(response)) {
140
+ consola.debug("Non-streaming response");
141
+ return c.json(response);
142
+ }
143
+ consola.debug("Streaming response");
144
+ return streamSSE(c, async (stream) => {
145
+ for await (const chunk of response) {
146
+ consola.debug("Streaming chunk:", JSON.stringify(chunk));
147
+ await stream.writeSSE(chunk);
148
+ }
149
+ });
150
+ }
151
+ catch (error) {
152
+ return await forwardError(c, error);
153
+ }
154
+ });
155
+ // GET /models
156
+ app.openapi(modelsRoute, async (c) => {
157
+ try {
158
+ // Check for grouped query parameter
159
+ const grouped = c.req.query("grouped") === "true";
160
+ if (grouped) {
161
+ // Return models grouped by token
162
+ const tokenModels = await getModelsForAllTokens();
163
+ return c.json({
164
+ object: "list",
165
+ grouped: true,
166
+ tokens: tokenModels.map((tm) => ({
167
+ token_id: tm.tokenId,
168
+ username: tm.username,
169
+ account_type: tm.accountType,
170
+ error: tm.error,
171
+ models: tm.models?.data.map((model) => ({
172
+ id: model.id,
173
+ object: "model",
174
+ type: "model",
175
+ created: 0,
176
+ created_at: new Date(0).toISOString(),
177
+ owned_by: model.vendor,
178
+ display_name: model.name,
179
+ })) ?? [],
180
+ })),
181
+ });
182
+ }
183
+ // Get models from first active token
184
+ const modelsResponse = await getModels();
185
+ const models = modelsResponse.data.map((model) => ({
186
+ id: model.id,
187
+ object: "model",
188
+ type: "model",
189
+ created: 0,
190
+ created_at: new Date(0).toISOString(),
191
+ owned_by: model.vendor,
192
+ display_name: model.name,
193
+ }));
194
+ return c.json({
195
+ object: "list",
196
+ data: models,
197
+ has_more: false,
198
+ });
199
+ }
200
+ catch (error) {
201
+ return await forwardError(c, error);
202
+ }
203
+ });
204
+ // POST /embeddings
205
+ app.openapi(embeddingsRoute, async (c) => {
206
+ try {
207
+ const payload = await c.req.json();
208
+ const response = await createEmbeddings(payload);
209
+ return c.json(response);
210
+ }
211
+ catch (error) {
212
+ return await forwardError(c, error);
213
+ }
214
+ });
215
+ }
@@ -0,0 +1,2 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ export declare function registerUtilityRoutes(app: OpenAPIHono): void;
@@ -0,0 +1,28 @@
1
+ import { tokenManager } from "../../lib/token-manager.js";
2
+ export function registerUtilityRoutes(app) {
3
+ // GET /
4
+ app.get("/", (c) => {
5
+ const counts = tokenManager.getTokenCount();
6
+ return c.text(`Copilot Router is running (${counts.active}/${counts.total} active tokens)`);
7
+ });
8
+ // GET /token
9
+ app.get("/token", (c) => {
10
+ try {
11
+ const entries = tokenManager.getActiveTokenEntries();
12
+ const tokens = entries.map(e => ({
13
+ id: e.id,
14
+ username: e.username,
15
+ copilot_token: e.copilotToken?.substring(0, 20) + "...",
16
+ expires_at: e.copilotTokenExpiresAt?.toISOString(),
17
+ }));
18
+ return c.json({
19
+ count: tokens.length,
20
+ tokens,
21
+ });
22
+ }
23
+ catch (error) {
24
+ console.error("Error fetching token:", error);
25
+ return c.json({ error: { message: "Failed to fetch token", type: "error" } }, 500);
26
+ }
27
+ });
28
+ }
@@ -0,0 +1,130 @@
1
+ import { type TokenEntry } from "../../lib/token-manager.js";
2
+ export declare const createChatCompletions: (payload: ChatCompletionsPayload, tokenEntry?: TokenEntry) => Promise<ChatCompletionResponse | AsyncGenerator<import("fetch-event-stream").ServerSentEventMessage, void, unknown>>;
3
+ export interface ChatCompletionChunk {
4
+ id: string;
5
+ object: "chat.completion.chunk";
6
+ created: number;
7
+ model: string;
8
+ choices: Array<Choice>;
9
+ system_fingerprint?: string;
10
+ usage?: {
11
+ prompt_tokens: number;
12
+ completion_tokens: number;
13
+ total_tokens: number;
14
+ prompt_tokens_details?: {
15
+ cached_tokens: number;
16
+ };
17
+ completion_tokens_details?: {
18
+ accepted_prediction_tokens: number;
19
+ rejected_prediction_tokens: number;
20
+ };
21
+ };
22
+ }
23
+ interface Delta {
24
+ content?: string | null;
25
+ role?: "user" | "assistant" | "system" | "tool";
26
+ tool_calls?: Array<{
27
+ index: number;
28
+ id?: string;
29
+ type?: "function";
30
+ function?: {
31
+ name?: string;
32
+ arguments?: string;
33
+ };
34
+ }>;
35
+ }
36
+ interface Choice {
37
+ index: number;
38
+ delta: Delta;
39
+ finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | null;
40
+ logprobs: object | null;
41
+ }
42
+ export interface ChatCompletionResponse {
43
+ id: string;
44
+ object: "chat.completion";
45
+ created: number;
46
+ model: string;
47
+ choices: Array<ChoiceNonStreaming>;
48
+ system_fingerprint?: string;
49
+ usage?: {
50
+ prompt_tokens: number;
51
+ completion_tokens: number;
52
+ total_tokens: number;
53
+ prompt_tokens_details?: {
54
+ cached_tokens: number;
55
+ };
56
+ };
57
+ }
58
+ interface ResponseMessage {
59
+ role: "assistant";
60
+ content: string | null;
61
+ tool_calls?: Array<ToolCall>;
62
+ }
63
+ interface ChoiceNonStreaming {
64
+ index: number;
65
+ message: ResponseMessage;
66
+ logprobs: object | null;
67
+ finish_reason: "stop" | "length" | "tool_calls" | "content_filter";
68
+ }
69
+ export interface ChatCompletionsPayload {
70
+ messages: Array<Message>;
71
+ model: string;
72
+ temperature?: number | null;
73
+ top_p?: number | null;
74
+ max_tokens?: number | null;
75
+ stop?: string | Array<string> | null;
76
+ n?: number | null;
77
+ stream?: boolean | null;
78
+ frequency_penalty?: number | null;
79
+ presence_penalty?: number | null;
80
+ logit_bias?: Record<string, number> | null;
81
+ logprobs?: boolean | null;
82
+ response_format?: {
83
+ type: "json_object";
84
+ } | null;
85
+ seed?: number | null;
86
+ tools?: Array<Tool> | null;
87
+ tool_choice?: "none" | "auto" | "required" | {
88
+ type: "function";
89
+ function: {
90
+ name: string;
91
+ };
92
+ } | null;
93
+ user?: string | null;
94
+ }
95
+ export interface Tool {
96
+ type: "function";
97
+ function: {
98
+ name: string;
99
+ description?: string;
100
+ parameters: Record<string, unknown>;
101
+ };
102
+ }
103
+ export interface Message {
104
+ role: "user" | "assistant" | "system" | "tool" | "developer";
105
+ content: string | Array<ContentPart> | null;
106
+ name?: string;
107
+ tool_calls?: Array<ToolCall>;
108
+ tool_call_id?: string;
109
+ }
110
+ export interface ToolCall {
111
+ id: string;
112
+ type: "function";
113
+ function: {
114
+ name: string;
115
+ arguments: string;
116
+ };
117
+ }
118
+ export type ContentPart = TextPart | ImagePart;
119
+ export interface TextPart {
120
+ type: "text";
121
+ text: string;
122
+ }
123
+ export interface ImagePart {
124
+ type: "image_url";
125
+ image_url: {
126
+ url: string;
127
+ detail?: "low" | "high" | "auto";
128
+ };
129
+ }
130
+ export {};
@@ -0,0 +1,32 @@
1
+ import consola from "consola";
2
+ import { events } from "fetch-event-stream";
3
+ import { copilotHeadersForEntry, copilotBaseUrlForEntry } from "../../lib/api-config.js";
4
+ import { HTTPError } from "../../lib/error.js";
5
+ import { tokenManager } from "../../lib/token-manager.js";
6
+ export const createChatCompletions = async (payload, tokenEntry) => {
7
+ // Get token entry - use provided one or get random for load balancing
8
+ const entry = tokenEntry || tokenManager.getRandomTokenEntry();
9
+ if (!entry)
10
+ throw new Error("No active tokens available");
11
+ const enableVision = payload.messages.some((x) => typeof x.content !== "string" &&
12
+ x.content?.some((x) => x.type === "image_url"));
13
+ const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
14
+ const headers = {
15
+ ...copilotHeadersForEntry(entry, enableVision),
16
+ "X-Initiator": isAgentCall ? "agent" : "user",
17
+ };
18
+ const response = await fetch(`${copilotBaseUrlForEntry(entry)}/chat/completions`, {
19
+ method: "POST",
20
+ headers,
21
+ body: JSON.stringify(payload),
22
+ });
23
+ if (!response.ok) {
24
+ consola.error("Failed to create chat completions", response);
25
+ tokenManager.reportError(entry.id);
26
+ throw new HTTPError("Failed to create chat completions", response);
27
+ }
28
+ if (payload.stream) {
29
+ return events(response);
30
+ }
31
+ return (await response.json());
32
+ };
@@ -0,0 +1,20 @@
1
+ import { type TokenEntry } from "../../lib/token-manager.js";
2
+ export declare const createEmbeddings: (payload: EmbeddingRequest, tokenEntry?: TokenEntry) => Promise<EmbeddingResponse>;
3
+ export interface EmbeddingRequest {
4
+ input: string | Array<string>;
5
+ model: string;
6
+ }
7
+ export interface Embedding {
8
+ object: string;
9
+ embedding: Array<number>;
10
+ index: number;
11
+ }
12
+ export interface EmbeddingResponse {
13
+ object: string;
14
+ data: Array<Embedding>;
15
+ model: string;
16
+ usage: {
17
+ prompt_tokens: number;
18
+ total_tokens: number;
19
+ };
20
+ }
@@ -0,0 +1,19 @@
1
+ import { copilotHeadersForEntry, copilotBaseUrlForEntry } from "../../lib/api-config.js";
2
+ import { HTTPError } from "../../lib/error.js";
3
+ import { tokenManager } from "../../lib/token-manager.js";
4
+ export const createEmbeddings = async (payload, tokenEntry) => {
5
+ // Get token entry - use provided one or get random for load balancing
6
+ const entry = tokenEntry || tokenManager.getRandomTokenEntry();
7
+ if (!entry)
8
+ throw new Error("No active tokens available");
9
+ const response = await fetch(`${copilotBaseUrlForEntry(entry)}/embeddings`, {
10
+ method: "POST",
11
+ headers: copilotHeadersForEntry(entry),
12
+ body: JSON.stringify(payload),
13
+ });
14
+ if (!response.ok) {
15
+ tokenManager.reportError(entry.id);
16
+ throw new HTTPError("Failed to create embeddings", response);
17
+ }
18
+ return (await response.json());
19
+ };
@@ -0,0 +1,51 @@
1
+ import { type TokenEntry } from "../../lib/token-manager.js";
2
+ export declare const getModels: (tokenEntry?: TokenEntry) => Promise<ModelsResponse>;
3
+ /**
4
+ * Get models for all active token entries (for grouped display)
5
+ */
6
+ export declare const getModelsForAllTokens: () => Promise<TokenModelsResult[]>;
7
+ export interface TokenModelsResult {
8
+ tokenId: number;
9
+ username: string | null;
10
+ accountType: string;
11
+ models: ModelsResponse | null;
12
+ error?: string;
13
+ }
14
+ export interface ModelsResponse {
15
+ data: Array<Model>;
16
+ object: string;
17
+ }
18
+ interface ModelLimits {
19
+ max_context_window_tokens?: number;
20
+ max_output_tokens?: number;
21
+ max_prompt_tokens?: number;
22
+ max_inputs?: number;
23
+ }
24
+ interface ModelSupports {
25
+ tool_calls?: boolean;
26
+ parallel_tool_calls?: boolean;
27
+ dimensions?: boolean;
28
+ }
29
+ interface ModelCapabilities {
30
+ family: string;
31
+ limits: ModelLimits;
32
+ object: string;
33
+ supports: ModelSupports;
34
+ tokenizer: string;
35
+ type: string;
36
+ }
37
+ export interface Model {
38
+ capabilities: ModelCapabilities;
39
+ id: string;
40
+ model_picker_enabled: boolean;
41
+ name: string;
42
+ object: string;
43
+ preview: boolean;
44
+ vendor: string;
45
+ version: string;
46
+ policy?: {
47
+ state: string;
48
+ terms: string;
49
+ };
50
+ }
51
+ export {};
@@ -0,0 +1,45 @@
1
+ import { copilotBaseUrlForEntry, copilotHeadersForEntry } from "../../lib/api-config.js";
2
+ import { HTTPError } from "../../lib/error.js";
3
+ import { tokenManager } from "../../lib/token-manager.js";
4
+ export const getModels = async (tokenEntry) => {
5
+ // Get token entry - use provided one or get first active one
6
+ const entry = tokenEntry || tokenManager.getActiveTokenEntries()[0];
7
+ if (!entry)
8
+ throw new Error("No active tokens available");
9
+ const response = await fetch(`${copilotBaseUrlForEntry(entry)}/models`, {
10
+ headers: copilotHeadersForEntry(entry),
11
+ });
12
+ if (!response.ok) {
13
+ tokenManager.reportError(entry.id);
14
+ throw new HTTPError("Failed to get models", response);
15
+ }
16
+ return (await response.json());
17
+ };
18
+ /**
19
+ * Get models for all active token entries (for grouped display)
20
+ */
21
+ export const getModelsForAllTokens = async () => {
22
+ const entries = tokenManager.getActiveTokenEntries();
23
+ const results = [];
24
+ for (const entry of entries) {
25
+ try {
26
+ const models = await getModels(entry);
27
+ results.push({
28
+ tokenId: entry.id,
29
+ username: entry.username,
30
+ accountType: entry.accountType,
31
+ models,
32
+ });
33
+ }
34
+ catch (error) {
35
+ results.push({
36
+ tokenId: entry.id,
37
+ username: entry.username,
38
+ accountType: entry.accountType,
39
+ models: null,
40
+ error: error instanceof Error ? error.message : "Unknown error",
41
+ });
42
+ }
43
+ }
44
+ return results;
45
+ };
@@ -0,0 +1,11 @@
1
+ export declare const GITHUB_BASE_URL = "https://github.com";
2
+ export declare const GITHUB_CLIENT_ID = "178c6fc778ccc68e1d6a";
3
+ export declare const GITHUB_APP_SCOPES: string;
4
+ export declare function getDeviceCode(): Promise<DeviceCodeResponse>;
5
+ export interface DeviceCodeResponse {
6
+ device_code: string;
7
+ user_code: string;
8
+ verification_uri: string;
9
+ expires_in: number;
10
+ interval: number;
11
+ }
@@ -0,0 +1,21 @@
1
+ import { standardHeaders } from "../../lib/api-config.js";
2
+ import { HTTPError } from "../../lib/error.js";
3
+ export const GITHUB_BASE_URL = "https://github.com";
4
+ // GitHub CLI (gh) OAuth App Client ID
5
+ export const GITHUB_CLIENT_ID = "178c6fc778ccc68e1d6a";
6
+ // Scopes matching `gh auth login` defaults
7
+ export const GITHUB_APP_SCOPES = ["gist", "read:org", "repo"].join(" ");
8
+ export async function getDeviceCode() {
9
+ const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
10
+ method: "POST",
11
+ headers: standardHeaders(),
12
+ body: JSON.stringify({
13
+ client_id: GITHUB_CLIENT_ID,
14
+ scope: GITHUB_APP_SCOPES,
15
+ }),
16
+ });
17
+ if (!response.ok) {
18
+ throw new HTTPError("Failed to get device code", response);
19
+ }
20
+ return (await response.json());
21
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Get GitHub user info for a specific token
3
+ */
4
+ export declare function getGitHubUserForToken(githubToken: string): Promise<GithubUserResponse>;
5
+ export interface GithubUserResponse {
6
+ login: string;
7
+ id: number;
8
+ avatar_url: string;
9
+ name: string | null;
10
+ email: string | null;
11
+ }
@@ -0,0 +1,17 @@
1
+ import { GITHUB_API_BASE_URL, standardHeaders } from "../../lib/api-config.js";
2
+ import { HTTPError } from "../../lib/error.js";
3
+ /**
4
+ * Get GitHub user info for a specific token
5
+ */
6
+ export async function getGitHubUserForToken(githubToken) {
7
+ const response = await fetch(`${GITHUB_API_BASE_URL}/user`, {
8
+ headers: {
9
+ authorization: `token ${githubToken}`,
10
+ ...standardHeaders(),
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ throw new HTTPError("Failed to get GitHub user", response);
15
+ }
16
+ return (await response.json());
17
+ }
@@ -0,0 +1,13 @@
1
+ import { type DeviceCodeResponse } from "./get-device-code.js";
2
+ /**
3
+ * Check access token once (for client-side polling)
4
+ */
5
+ export declare function checkAccessToken(deviceCode: string): Promise<{
6
+ access_token?: string;
7
+ error?: string;
8
+ error_description?: string;
9
+ }>;
10
+ /**
11
+ * Poll for access token after device code flow (blocking, for CLI use)
12
+ */
13
+ export declare function pollAccessToken(deviceCode: DeviceCodeResponse): Promise<string>;
@@ -0,0 +1,56 @@
1
+ import consola from "consola";
2
+ import { standardHeaders } from "../../lib/api-config.js";
3
+ import { GITHUB_BASE_URL, GITHUB_CLIENT_ID, } from "./get-device-code.js";
4
+ /**
5
+ * Sleep helper
6
+ */
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ /**
11
+ * Check access token once (for client-side polling)
12
+ */
13
+ export async function checkAccessToken(deviceCode) {
14
+ const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
15
+ method: "POST",
16
+ headers: standardHeaders(),
17
+ body: JSON.stringify({
18
+ client_id: GITHUB_CLIENT_ID,
19
+ device_code: deviceCode,
20
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
21
+ }),
22
+ });
23
+ if (!response.ok) {
24
+ const text = await response.text();
25
+ consola.error("Failed to check access token:", text);
26
+ return { error: "request_failed", error_description: text };
27
+ }
28
+ const json = await response.json();
29
+ consola.debug("Check access token response:", json);
30
+ return {
31
+ access_token: json.access_token,
32
+ error: json.error,
33
+ error_description: json.error_description,
34
+ };
35
+ }
36
+ /**
37
+ * Poll for access token after device code flow (blocking, for CLI use)
38
+ */
39
+ export async function pollAccessToken(deviceCode) {
40
+ // Interval is in seconds, we need to multiply by 1000 to get milliseconds
41
+ // Adding another second to be safe
42
+ const sleepDuration = (deviceCode.interval + 1) * 1000;
43
+ consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
44
+ while (true) {
45
+ const result = await checkAccessToken(deviceCode.device_code);
46
+ if (result.access_token) {
47
+ return result.access_token;
48
+ }
49
+ else if (result.error === "expired_token") {
50
+ throw new Error("Device code expired. Please try again.");
51
+ }
52
+ else {
53
+ await sleep(sleepDuration);
54
+ }
55
+ }
56
+ }