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.
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/lib/api-config.d.ts +15 -0
- package/dist/lib/api-config.js +30 -0
- package/dist/lib/database.d.ts +60 -0
- package/dist/lib/database.js +228 -0
- package/dist/lib/error.d.ts +11 -0
- package/dist/lib/error.js +34 -0
- package/dist/lib/state.d.ts +9 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/token-manager.d.ts +95 -0
- package/dist/lib/token-manager.js +241 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +10 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +97 -0
- package/dist/routes/anthropic/routes.d.ts +2 -0
- package/dist/routes/anthropic/routes.js +155 -0
- package/dist/routes/anthropic/stream-translation.d.ts +3 -0
- package/dist/routes/anthropic/stream-translation.js +136 -0
- package/dist/routes/anthropic/translation.d.ts +4 -0
- package/dist/routes/anthropic/translation.js +241 -0
- package/dist/routes/anthropic/types.d.ts +165 -0
- package/dist/routes/anthropic/types.js +2 -0
- package/dist/routes/anthropic/utils.d.ts +2 -0
- package/dist/routes/anthropic/utils.js +12 -0
- package/dist/routes/auth/routes.d.ts +2 -0
- package/dist/routes/auth/routes.js +158 -0
- package/dist/routes/gemini/routes.d.ts +2 -0
- package/dist/routes/gemini/routes.js +163 -0
- package/dist/routes/gemini/translation.d.ts +5 -0
- package/dist/routes/gemini/translation.js +215 -0
- package/dist/routes/gemini/types.d.ts +63 -0
- package/dist/routes/gemini/types.js +2 -0
- package/dist/routes/openai/routes.d.ts +2 -0
- package/dist/routes/openai/routes.js +215 -0
- package/dist/routes/utility/routes.d.ts +2 -0
- package/dist/routes/utility/routes.js +28 -0
- package/dist/services/copilot/create-chat-completions.d.ts +130 -0
- package/dist/services/copilot/create-chat-completions.js +32 -0
- package/dist/services/copilot/create-embeddings.d.ts +20 -0
- package/dist/services/copilot/create-embeddings.js +19 -0
- package/dist/services/copilot/get-models.d.ts +51 -0
- package/dist/services/copilot/get-models.js +45 -0
- package/dist/services/github/get-device-code.d.ts +11 -0
- package/dist/services/github/get-device-code.js +21 -0
- package/dist/services/github/get-user.d.ts +11 -0
- package/dist/services/github/get-user.js +17 -0
- package/dist/services/github/poll-access-token.d.ts +13 -0
- package/dist/services/github/poll-access-token.js +56 -0
- package/package.json +56 -0
- 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
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;
|