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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # Copilot Router
2
+
3
+ GitHub Copilot API with OpenAI, Anthropic, and Gemini compatibility. Supports **multiple GitHub tokens** with load balancing and SQL Server storage.
4
+
5
+ ## Features
6
+
7
+ - **Multi-token support**: Add multiple GitHub accounts for load balancing
8
+ - **SQL Server storage**: Persist tokens across restarts (supports Azure AD authentication)
9
+ - **Memory-only mode**: Works without database configuration
10
+ - **GitHub Device Code flow**: Use device code flow to authenticate
11
+ - **Web UI**: User-friendly login page for managing tokens
12
+ - **Load balancing**: Randomly distribute requests across active tokens
13
+ - **OpenAI, Anthropic, and Gemini compatibility**: Use familiar APIs
14
+ - **OpenAPI documentation**: Auto-generated API docs at `/openapi.json`
15
+
16
+ ## Quick Start
17
+
18
+ The fastest way to run Copilot Router without cloning the repository:
19
+
20
+ ```bash
21
+ npx copilot-router@latest start
22
+ ```
23
+
24
+ With custom port:
25
+
26
+ ```bash
27
+ npx copilot-router@latest start --port 8080
28
+ ```
29
+
30
+ ## Setup (Development)
31
+
32
+ ### 1. Configure Environment
33
+
34
+ Copy the example environment file:
35
+
36
+ ```bash
37
+ cp .env.example .env
38
+ ```
39
+
40
+ Edit `.env` with your configuration:
41
+
42
+ ```env
43
+ # SQL Server connection string (optional - uses memory mode if not provided)
44
+ DB_CONNECTION_STRING=Server=localhost;Database=copilot_router;Authentication=Active Directory Default
45
+ PORT=4242
46
+ ```
47
+
48
+ **Supported Authentication Types:**
49
+ - `Active Directory Default` - Uses system's default Azure AD auth
50
+ - `Active Directory Managed Identity` - Uses Azure Managed Identity (requires `User Id` for client-id)
51
+
52
+ > **Note**: If `DB_CONNECTION_STRING` is not provided, the server runs in memory-only mode. In memory-only mode, the server will automatically detect and use your local `gh auth` token if available.
53
+
54
+ ### 2. Install Dependencies
55
+
56
+ ```bash
57
+ npm install
58
+ ```
59
+
60
+ ### 3. Start the Server
61
+
62
+ ```bash
63
+ npm run dev
64
+ ```
65
+
66
+ Or for production:
67
+
68
+ ```bash
69
+ npm start
70
+ ```
71
+
72
+ ## Web UI
73
+
74
+ Access the login UI at `http://localhost:4242/login` to:
75
+ - Login via GitHub Device Code flow
76
+ - Add tokens directly
77
+ - Manage existing tokens
78
+
79
+ ## Authentication
80
+
81
+ ### Method 1: Device Code Flow (Recommended)
82
+
83
+ 1. **Start login**:
84
+ ```bash
85
+ curl -X POST http://localhost:4242/auth/login
86
+ ```
87
+
88
+ Response:
89
+ ```json
90
+ {
91
+ "user_code": "ABCD-1234",
92
+ "verification_uri": "https://github.com/login/device",
93
+ "device_code": "...",
94
+ "expires_in": 900,
95
+ "interval": 5
96
+ }
97
+ ```
98
+
99
+ 2. **Visit the URL** and enter the code to authorize.
100
+
101
+ 3. **Complete login**:
102
+ ```bash
103
+ curl -X POST http://localhost:4242/auth/complete \
104
+ -H "Content-Type: application/json" \
105
+ -d '{
106
+ "device_code": "...",
107
+ "account_type": "individual"
108
+ }'
109
+ ```
110
+
111
+ Response:
112
+ ```json
113
+ {
114
+ "status": "success",
115
+ "id": 1,
116
+ "username": "your-username",
117
+ "account_type": "individual",
118
+ "message": "Successfully logged in as your-username"
119
+ }
120
+ ```
121
+
122
+ ### Method 2: Direct Token Input
123
+
124
+ If you already have a GitHub token:
125
+
126
+ ```bash
127
+ curl -X POST http://localhost:4242/auth/tokens \
128
+ -H "Content-Type: application/json" \
129
+ -d '{
130
+ "github_token": "ghu_xxx...",
131
+ "account_type": "individual"
132
+ }'
133
+ ```
134
+
135
+ ### Manage Tokens
136
+
137
+ **List all tokens**:
138
+ ```bash
139
+ curl http://localhost:4242/auth/tokens
140
+ ```
141
+
142
+ **Delete a token**:
143
+ ```bash
144
+ curl -X DELETE http://localhost:4242/auth/tokens/1
145
+ ```
146
+
147
+ **Delete all tokens**:
148
+ ```bash
149
+ curl -X DELETE http://localhost:4242/auth/tokens/all
150
+ ```
151
+
152
+ ## API Endpoints
153
+
154
+ ### OpenAI-Compatible
155
+
156
+ Available at both root (`/`) and versioned (`/v1`) paths:
157
+
158
+ - `POST /chat/completions` or `POST /v1/chat/completions` - Chat completion (load balanced)
159
+ - `GET /models` or `GET /v1/models` - List models
160
+ - `GET /models?grouped=true` or `GET /v1/models?grouped=true` - List models grouped by token
161
+ - `POST /embeddings` or `POST /v1/embeddings` - Create embeddings (load balanced)
162
+
163
+ ### Anthropic-Compatible
164
+
165
+ - `POST /v1/messages` - Create message (load balanced)
166
+ - `POST /v1/messages/count_tokens` - Count tokens
167
+
168
+ ### Gemini-Compatible
169
+
170
+ - `POST /v1beta/models/:model:generateContent` - Generate content (load balanced)
171
+ - `POST /v1beta/models/:model:streamGenerateContent` - Stream content
172
+ - `POST /v1beta/models/:model:countTokens` - Count tokens
173
+
174
+ ### Utility
175
+
176
+ - `GET /` - Health check with token count
177
+ - `GET /token` - List active tokens with Copilot token info
178
+ - `GET /login` - Web UI for token management
179
+ - `GET /openapi.json` - OpenAPI documentation
180
+
181
+ ### Auth
182
+
183
+ - `POST /auth/login` - Start device code flow
184
+ - `POST /auth/complete` - Complete device code authentication
185
+ - `GET /auth/tokens` - List all tokens with statistics
186
+ - `POST /auth/tokens` - Add token directly
187
+ - `DELETE /auth/tokens/:id` - Delete a specific token
188
+ - `DELETE /auth/tokens/all` - Delete all tokens
189
+
190
+ ## Load Balancing
191
+
192
+ All API calls (`/chat/completions`, `/embeddings`, `/messages`, `:generateContent`) automatically use **random token selection** for load balancing. Each request will use a different token from your pool of active tokens.
193
+
194
+ ### Request Statistics
195
+
196
+ View per-token statistics:
197
+
198
+ ```bash
199
+ curl http://localhost:4242/auth/tokens
200
+ ```
201
+
202
+ Response includes:
203
+ - `request_count` - Total requests made with this token
204
+ - `error_count` - Number of errors
205
+ - `last_used` - Last time the token was used
206
+ - `has_copilot_token` - Whether the Copilot token is valid
207
+ - `copilot_token_expires_at` - Expiration time of the Copilot token
208
+
209
+ ## Grouped Display
210
+
211
+ For multi-token setups, you can view models grouped by token:
212
+
213
+ ```bash
214
+ # Models grouped by token
215
+ curl "http://localhost:4242/v1/models?grouped=true"
216
+ ```
217
+
218
+ ## Database Schema
219
+
220
+ When using SQL Server, the system automatically creates a `GithubTokens` table:
221
+
222
+ ```sql
223
+ CREATE TABLE GithubTokens (
224
+ id INT IDENTITY(1,1) PRIMARY KEY,
225
+ Token NVARCHAR(500) NOT NULL,
226
+ UserName NVARCHAR(100) UNIQUE,
227
+ AccountType NVARCHAR(50) DEFAULT 'individual',
228
+ IsActive BIT DEFAULT 1
229
+ )
230
+ ```
231
+
232
+ ## Tech Stack
233
+
234
+ - **Runtime**: Node.js with TypeScript
235
+ - **Framework**: [Hono](https://hono.dev/) with OpenAPI support
236
+ - **Database**: Microsoft SQL Server (optional)
237
+ - **Build Tool**: TSX for development, TypeScript for production
238
+
239
+ ## License
240
+
241
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import consola from "consola";
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ // Get package.json location relative to this file
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const packageJsonPath = join(__dirname, "..", "package.json");
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
12
+ const program = new Command();
13
+ program
14
+ .name("copilot-router")
15
+ .description(packageJson.description)
16
+ .version(packageJson.version);
17
+ program
18
+ .command("start")
19
+ .description("Start the Copilot Router server")
20
+ .option("-p, --port <port>", "Port to listen on", "4242")
21
+ .action(async (options) => {
22
+ // Set port from CLI option if provided
23
+ if (options.port) {
24
+ process.env.PORT = options.port;
25
+ }
26
+ // Set the package root for static files
27
+ process.env.COPILOT_ROUTER_ROOT = join(__dirname, "..");
28
+ consola.info("Starting Copilot Router via CLI...");
29
+ // Dynamically import the main module to start the server
30
+ await import("./main.js");
31
+ });
32
+ program.parse();
@@ -0,0 +1,15 @@
1
+ import type { TokenEntry } from "./token-manager.js";
2
+ export declare const standardHeaders: () => {
3
+ "content-type": string;
4
+ accept: string;
5
+ };
6
+ /**
7
+ * Get base URL for token entry
8
+ */
9
+ export declare const copilotBaseUrlForEntry: (entry: TokenEntry) => string;
10
+ /**
11
+ * Create headers for a token entry
12
+ * Note: Using GitHub Access Token directly instead of Copilot Token
13
+ */
14
+ export declare const copilotHeadersForEntry: (entry: TokenEntry, vision?: boolean) => Record<string, string>;
15
+ export declare const GITHUB_API_BASE_URL = "https://api.github.com";
@@ -0,0 +1,30 @@
1
+ export const standardHeaders = () => ({
2
+ "content-type": "application/json",
3
+ accept: "application/json",
4
+ });
5
+ const COPILOT_VERSION = "0.0.388";
6
+ const API_VERSION = "2025-05-01";
7
+ /**
8
+ * Get base URL for token entry
9
+ */
10
+ export const copilotBaseUrlForEntry = (entry) => entry.accountType === "individual"
11
+ ? "https://api.githubcopilot.com"
12
+ : `https://api.${entry.accountType}.githubcopilot.com`;
13
+ /**
14
+ * Create headers for a token entry
15
+ * Note: Using GitHub Access Token directly instead of Copilot Token
16
+ */
17
+ export const copilotHeadersForEntry = (entry, vision = false) => {
18
+ const headers = {
19
+ Authorization: `Bearer ${entry.githubToken}`,
20
+ "content-type": standardHeaders()["content-type"],
21
+ "copilot-integration-id": "copilot-developer-cli",
22
+ "user-agent": `copilot/${COPILOT_VERSION} (linux v24.11.1) term/unknown`,
23
+ "openai-intent": "conversation-agent",
24
+ "x-github-api-version": API_VERSION,
25
+ };
26
+ if (vision)
27
+ headers["copilot-vision-request"] = "true";
28
+ return headers;
29
+ };
30
+ export const GITHUB_API_BASE_URL = "https://api.github.com";
@@ -0,0 +1,60 @@
1
+ import sql from "mssql";
2
+ /**
3
+ * Check if database configuration is provided
4
+ */
5
+ export declare function isDatabaseConfigured(): boolean;
6
+ /**
7
+ * Check if database is connected
8
+ */
9
+ export declare function isDatabaseConnected(): boolean;
10
+ /**
11
+ * Initialize database connection and create tables if not exist
12
+ * Returns true if database was initialized, false if skipped (no config)
13
+ */
14
+ export declare function initializeDatabase(): Promise<boolean>;
15
+ /**
16
+ * Get database connection pool
17
+ */
18
+ export declare function getPool(): sql.ConnectionPool;
19
+ /**
20
+ * Token record from database
21
+ */
22
+ export interface TokenRecord {
23
+ Id: number;
24
+ Token: string;
25
+ UserName: string | null;
26
+ AccountType: string;
27
+ IsActive: boolean;
28
+ }
29
+ /**
30
+ * Get all active tokens from database
31
+ */
32
+ export declare function getAllTokens(): Promise<TokenRecord[]>;
33
+ /**
34
+ * Get a specific token by ID
35
+ */
36
+ export declare function getTokenById(id: number): Promise<TokenRecord | null>;
37
+ /**
38
+ * Get a token by GitHub token value
39
+ */
40
+ export declare function getTokenByGithubToken(githubToken: string): Promise<TokenRecord | null>;
41
+ /**
42
+ * Save a new GitHub token to database (or update existing by username)
43
+ */
44
+ export declare function saveToken(githubToken: string, username?: string, accountType?: string): Promise<number>;
45
+ /**
46
+ * Update GitHub token for an existing entry
47
+ */
48
+ export declare function updateGithubToken(id: number, githubToken: string): Promise<void>;
49
+ /**
50
+ * Deactivate a token
51
+ */
52
+ export declare function deactivateToken(id: number): Promise<void>;
53
+ /**
54
+ * Delete all tokens (soft delete - sets IsActive = 0 for all records)
55
+ */
56
+ export declare function deleteAllTokens(): Promise<number>;
57
+ /**
58
+ * Close database connection
59
+ */
60
+ export declare function closeDatabase(): Promise<void>;
@@ -0,0 +1,228 @@
1
+ import sql from "mssql";
2
+ import consola from "consola";
3
+ /**
4
+ * Parse connection string to extract server, database, authentication type and user id
5
+ * Supported formats:
6
+ * - Server=xxx;Database=xxx;Authentication=Active Directory Default
7
+ * - Server=xxx;Database=xxx;User Id=xxx;Authentication=Active Directory Managed Identity
8
+ */
9
+ function parseConnectionString(connectionString) {
10
+ if (!connectionString)
11
+ return null;
12
+ const serverMatch = connectionString.match(/Server=([^;]+)/i);
13
+ const databaseMatch = connectionString.match(/Database=([^;]+)/i);
14
+ const authMatch = connectionString.match(/Authentication=([^;]+)/i);
15
+ const userIdMatch = connectionString.match(/User Id=([^;]+)/i);
16
+ if (!serverMatch || !databaseMatch)
17
+ return null;
18
+ return {
19
+ server: serverMatch[1],
20
+ database: databaseMatch[1],
21
+ authentication: authMatch?.[1] || "Active Directory Default",
22
+ userId: userIdMatch?.[1]
23
+ };
24
+ }
25
+ /**
26
+ * Build mssql authentication config based on connection string authentication type
27
+ */
28
+ function buildAuthConfig(dbConfig) {
29
+ const authType = dbConfig.authentication.toLowerCase();
30
+ // Active Directory Managed Identity
31
+ if (authType.includes("managed identity")) {
32
+ return {
33
+ type: "azure-active-directory-msi-app-service",
34
+ options: {
35
+ clientId: dbConfig.userId
36
+ }
37
+ };
38
+ }
39
+ // Active Directory Default (default fallback)
40
+ return {
41
+ type: "azure-active-directory-default",
42
+ options: {}
43
+ };
44
+ }
45
+ // Parse connection string
46
+ const dbConfig = parseConnectionString(process.env.DB_CONNECTION_STRING || "");
47
+ // SQL Server configuration
48
+ const sqlConfig = {
49
+ server: dbConfig?.server || "",
50
+ database: dbConfig?.database || "",
51
+ options: {
52
+ encrypt: true, // Required for Azure
53
+ trustServerCertificate: false,
54
+ },
55
+ authentication: dbConfig ? buildAuthConfig(dbConfig) : undefined
56
+ };
57
+ let pool = null;
58
+ /**
59
+ * Check if database configuration is provided
60
+ */
61
+ export function isDatabaseConfigured() {
62
+ return !!dbConfig;
63
+ }
64
+ /**
65
+ * Check if database is connected
66
+ */
67
+ export function isDatabaseConnected() {
68
+ return pool !== null;
69
+ }
70
+ /**
71
+ * Initialize database connection and create tables if not exist
72
+ * Returns true if database was initialized, false if skipped (no config)
73
+ */
74
+ export async function initializeDatabase() {
75
+ // Skip if database is not configured
76
+ if (!isDatabaseConfigured()) {
77
+ consola.info("Database not configured, using memory-only mode");
78
+ return false;
79
+ }
80
+ try {
81
+ consola.info(`Connecting to ${sqlConfig.server}/${sqlConfig.database}...`);
82
+ pool = await sql.connect(sqlConfig);
83
+ consola.success("Connected to SQL Server");
84
+ // Check if GithubTokens table exists, if not create it
85
+ // If table exists, use it directly (fields are guaranteed to be correct)
86
+ await pool.request().query(`
87
+ IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='GithubTokens' AND xtype='U')
88
+ BEGIN
89
+ CREATE TABLE GithubTokens (
90
+ id INT IDENTITY(1,1) PRIMARY KEY,
91
+ Token NVARCHAR(500) NOT NULL,
92
+ UserName NVARCHAR(100) UNIQUE,
93
+ AccountType NVARCHAR(50) DEFAULT 'individual',
94
+ IsActive BIT DEFAULT 1
95
+ )
96
+ END
97
+ `);
98
+ // Drop old unique constraint on Token if exists
99
+ await pool.request().query(`
100
+ BEGIN TRY
101
+ -- Try to drop the old unique constraint on Token
102
+ DECLARE @constraintName NVARCHAR(200)
103
+ SELECT @constraintName = name FROM sys.key_constraints
104
+ WHERE parent_object_id = OBJECT_ID('GithubTokens')
105
+ AND type = 'UQ'
106
+ AND OBJECT_NAME(parent_object_id) = 'GithubTokens'
107
+ AND EXISTS (
108
+ SELECT 1 FROM sys.index_columns ic
109
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
110
+ WHERE ic.object_id = OBJECT_ID('GithubTokens') AND c.name = 'Token'
111
+ )
112
+ IF @constraintName IS NOT NULL
113
+ EXEC('ALTER TABLE GithubTokens DROP CONSTRAINT ' + @constraintName)
114
+ END TRY
115
+ BEGIN CATCH
116
+ -- Ignore errors
117
+ END CATCH
118
+ `);
119
+ consola.success("Database tables initialized");
120
+ return true;
121
+ }
122
+ catch (error) {
123
+ consola.error("Failed to connect to SQL Server:", error);
124
+ throw error;
125
+ }
126
+ }
127
+ /**
128
+ * Get database connection pool
129
+ */
130
+ export function getPool() {
131
+ if (!pool) {
132
+ throw new Error("Database not initialized");
133
+ }
134
+ return pool;
135
+ }
136
+ /**
137
+ * Get all active tokens from database
138
+ */
139
+ export async function getAllTokens() {
140
+ const pool = getPool();
141
+ const result = await pool.request().query(`
142
+ SELECT * FROM GithubTokens WHERE IsActive = 1
143
+ `);
144
+ return result.recordset;
145
+ }
146
+ /**
147
+ * Get a specific token by ID
148
+ */
149
+ export async function getTokenById(id) {
150
+ const pool = getPool();
151
+ const result = await pool.request()
152
+ .input("id", sql.Int, id)
153
+ .query(`SELECT * FROM GithubTokens WHERE id = @id`);
154
+ return result.recordset[0] || null;
155
+ }
156
+ /**
157
+ * Get a token by GitHub token value
158
+ */
159
+ export async function getTokenByGithubToken(githubToken) {
160
+ const pool = getPool();
161
+ const result = await pool.request()
162
+ .input("Token", sql.NVarChar, githubToken)
163
+ .query(`SELECT * FROM GithubTokens WHERE Token = @Token`);
164
+ return result.recordset[0] || null;
165
+ }
166
+ /**
167
+ * Save a new GitHub token to database (or update existing by username)
168
+ */
169
+ export async function saveToken(githubToken, username, accountType = "individual") {
170
+ const pool = getPool();
171
+ const result = await pool.request()
172
+ .input("Token", sql.NVarChar, githubToken)
173
+ .input("UserName", sql.NVarChar, username || null)
174
+ .input("AccountType", sql.NVarChar, accountType)
175
+ .query(`
176
+ MERGE GithubTokens AS target
177
+ USING (SELECT @UserName AS UserName) AS source
178
+ ON target.UserName = source.UserName
179
+ WHEN MATCHED THEN
180
+ UPDATE SET Token = @Token, AccountType = @AccountType, IsActive = 1
181
+ WHEN NOT MATCHED THEN
182
+ INSERT (Token, UserName, AccountType) VALUES (@Token, @UserName, @AccountType)
183
+ OUTPUT inserted.id;
184
+ `);
185
+ return result.recordset[0]?.id;
186
+ }
187
+ /**
188
+ * Update GitHub token for an existing entry
189
+ */
190
+ export async function updateGithubToken(id, githubToken) {
191
+ const pool = getPool();
192
+ await pool.request()
193
+ .input("id", sql.Int, id)
194
+ .input("Token", sql.NVarChar, githubToken)
195
+ .query(`
196
+ UPDATE GithubTokens
197
+ SET Token = @Token,
198
+ IsActive = 1
199
+ WHERE id = @id
200
+ `);
201
+ }
202
+ /**
203
+ * Deactivate a token
204
+ */
205
+ export async function deactivateToken(id) {
206
+ const pool = getPool();
207
+ await pool.request()
208
+ .input("id", sql.Int, id)
209
+ .query(`UPDATE GithubTokens SET IsActive = 0 WHERE id = @id`);
210
+ }
211
+ /**
212
+ * Delete all tokens (soft delete - sets IsActive = 0 for all records)
213
+ */
214
+ export async function deleteAllTokens() {
215
+ const pool = getPool();
216
+ const result = await pool.request().query(`UPDATE GithubTokens SET IsActive = 0 WHERE IsActive = 1`);
217
+ return result.rowsAffected[0] || 0;
218
+ }
219
+ /**
220
+ * Close database connection
221
+ */
222
+ export async function closeDatabase() {
223
+ if (pool) {
224
+ await pool.close();
225
+ pool = null;
226
+ consola.info("Database connection closed");
227
+ }
228
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from "hono";
2
+ export declare class HTTPError extends Error {
3
+ response: Response;
4
+ constructor(message: string, response: Response);
5
+ }
6
+ export declare function forwardError(c: Context, error: unknown): Promise<Response & import("hono").TypedResponse<{
7
+ error: {
8
+ message: string;
9
+ type: string;
10
+ };
11
+ }, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "json">>;
@@ -0,0 +1,34 @@
1
+ import consola from "consola";
2
+ export class HTTPError extends Error {
3
+ response;
4
+ constructor(message, response) {
5
+ super(message);
6
+ this.response = response;
7
+ }
8
+ }
9
+ export async function forwardError(c, error) {
10
+ consola.error("Error occurred:", error);
11
+ if (error instanceof HTTPError) {
12
+ const errorText = await error.response.text();
13
+ let errorJson;
14
+ try {
15
+ errorJson = JSON.parse(errorText);
16
+ }
17
+ catch {
18
+ errorJson = errorText;
19
+ }
20
+ consola.error("HTTP error:", errorJson);
21
+ return c.json({
22
+ error: {
23
+ message: errorText,
24
+ type: "error",
25
+ },
26
+ }, error.response.status);
27
+ }
28
+ return c.json({
29
+ error: {
30
+ message: error.message,
31
+ type: "error",
32
+ },
33
+ }, 500);
34
+ }
@@ -0,0 +1,9 @@
1
+ import type { ModelsResponse } from "../services/copilot/get-models.js";
2
+ export interface State {
3
+ githubToken?: string;
4
+ copilotToken?: string;
5
+ accountType: string;
6
+ models?: ModelsResponse;
7
+ vsCodeVersion?: string;
8
+ }
9
+ export declare const state: State;
@@ -0,0 +1,3 @@
1
+ export const state = {
2
+ accountType: "individual",
3
+ };