copilot-cursor-proxy 1.1.1 → 1.2.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/dashboard.html +107 -4
- package/package.json +2 -2
- package/proxy-router.ts +34 -4
- package/responses-bridge.ts +2 -1
- package/start.ts +4 -1
- package/upstream-auth.ts +82 -0
package/dashboard.html
CHANGED
|
@@ -374,10 +374,10 @@ tr:hover td { background: var(--bg-hover); }
|
|
|
374
374
|
</div>
|
|
375
375
|
</div>
|
|
376
376
|
<div class="card">
|
|
377
|
-
<div class="card-label">API Key</div>
|
|
377
|
+
<div class="card-label">API Key (copilot-api)</div>
|
|
378
378
|
<div class="copy-row">
|
|
379
|
-
<div class="card-value mono">
|
|
380
|
-
<button class="copy-btn" onclick="
|
|
379
|
+
<div class="card-value mono" id="upstream-key-display">Loading…</div>
|
|
380
|
+
<button class="copy-btn" id="upstream-key-copy-btn" style="display:none" onclick="copyUpstreamKey(this)">Copy</button>
|
|
381
381
|
</div>
|
|
382
382
|
</div>
|
|
383
383
|
<div class="card" style="grid-column: 1 / -1">
|
|
@@ -388,10 +388,32 @@ tr:hover td { background: var(--bg-hover); }
|
|
|
388
388
|
</div>
|
|
389
389
|
</div>
|
|
390
390
|
|
|
391
|
+
<!-- Upstream (copilot-api) Keys -->
|
|
392
|
+
<div class="card" style="margin-bottom: 24px;">
|
|
393
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
394
|
+
<h3 style="margin: 0; border: none; padding: 0;">Copilot API Keys</h3>
|
|
395
|
+
<button onclick="handleCreateUpstreamKey()" style="background: var(--accent); color: #000; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: 600; white-space: nowrap;">+ New Key</button>
|
|
396
|
+
</div>
|
|
397
|
+
<p style="color: #888; font-size: 13px; margin-bottom: 12px;">
|
|
398
|
+
Keys stored in copilot-api's <code style="color:var(--accent)">config.json</code>. The first key is used by the proxy for upstream requests.
|
|
399
|
+
<br><span style="color:var(--yellow);">Note:</span> New keys require a copilot-api restart to take effect.
|
|
400
|
+
</p>
|
|
401
|
+
|
|
402
|
+
<div id="newUpstreamKeyAlert" style="display: none; background: #1a2e1a; border: 1px solid #22c55e; border-radius: 8px; padding: 12px; margin-bottom: 16px;">
|
|
403
|
+
<div style="color: #22c55e; font-weight: 600; margin-bottom: 4px;">Copy this key now — it won't be shown in full again!</div>
|
|
404
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
405
|
+
<code id="newUpstreamKeyValue" style="flex: 1; background: #111; padding: 8px; border-radius: 4px; font-size: 13px; color: #22c55e; word-break: break-all;"></code>
|
|
406
|
+
<button onclick="copyText(document.getElementById('newUpstreamKeyValue').textContent, this)" style="background: #333; border: none; color: #fff; padding: 6px 12px; border-radius: 4px; cursor: pointer;">Copy</button>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div id="upstreamKeysList"></div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
391
413
|
<!-- API Key Management -->
|
|
392
414
|
<div class="card" style="margin-bottom: 24px;">
|
|
393
415
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
394
|
-
<h3 style="margin: 0; border: none; padding: 0;">API Key Protection</h3>
|
|
416
|
+
<h3 style="margin: 0; border: none; padding: 0;">Proxy API Key Protection</h3>
|
|
395
417
|
<label class="toggle">
|
|
396
418
|
<input type="checkbox" id="requireKeyToggle" onchange="toggleRequireKey(this.checked)">
|
|
397
419
|
<span class="toggle-slider"></span>
|
|
@@ -617,6 +639,87 @@ async function fetchModels() {
|
|
|
617
639
|
}
|
|
618
640
|
fetchModels();
|
|
619
641
|
|
|
642
|
+
/* ── Tab 1: Upstream (copilot-api) Keys ───────────────────── */
|
|
643
|
+
let fullUpstreamKey = '';
|
|
644
|
+
|
|
645
|
+
async function loadUpstreamKeys() {
|
|
646
|
+
try {
|
|
647
|
+
const resp = await fetch('/api/upstream-keys');
|
|
648
|
+
const data = await resp.json();
|
|
649
|
+
const display = document.getElementById('upstream-key-display');
|
|
650
|
+
const copyBtn = document.getElementById('upstream-key-copy-btn');
|
|
651
|
+
|
|
652
|
+
if (data.keys && data.keys.length > 0) {
|
|
653
|
+
display.textContent = data.keys[0];
|
|
654
|
+
copyBtn.style.display = '';
|
|
655
|
+
} else {
|
|
656
|
+
display.textContent = 'No keys configured';
|
|
657
|
+
display.style.color = 'var(--yellow)';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
renderUpstreamKeys(data.keys || []);
|
|
661
|
+
} catch (e) {
|
|
662
|
+
document.getElementById('upstream-key-display').textContent = 'Failed to load';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function renderUpstreamKeys(maskedKeys) {
|
|
667
|
+
const list = document.getElementById('upstreamKeysList');
|
|
668
|
+
if (maskedKeys.length === 0) {
|
|
669
|
+
list.innerHTML = '<div style="color: #666; font-size: 13px; padding: 12px 0;">No API keys configured. Create one to authenticate with copilot-api.</div>';
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
list.innerHTML = '';
|
|
673
|
+
maskedKeys.forEach((k, i) => {
|
|
674
|
+
const row = document.createElement('div');
|
|
675
|
+
row.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid #222;';
|
|
676
|
+
const badge = i === 0
|
|
677
|
+
? '<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:#1a2332;color:var(--accent);margin-left:8px;">Active</span>'
|
|
678
|
+
: '';
|
|
679
|
+
row.innerHTML =
|
|
680
|
+
'<div style="flex: 1; min-width: 0;">' +
|
|
681
|
+
'<code style="color: #aaa; font-size: 13px;">' + esc(k) + '</code>' + badge +
|
|
682
|
+
'</div>' +
|
|
683
|
+
'<button class="copy-btn" title="Copy masked key" onclick="copyText(\'' + esc(k) + '\', this)">Copy</button>' +
|
|
684
|
+
'<button style="background: none; border: none; color: #666; cursor: pointer; font-size: 16px; padding: 4px 8px;" title="Delete" data-key="' + esc(k) + '">🗑</button>';
|
|
685
|
+
row.querySelector('button[title="Delete"]').addEventListener('click', function() { handleDeleteUpstreamKey(this.dataset.key); });
|
|
686
|
+
list.appendChild(row);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function copyUpstreamKey(btn) {
|
|
691
|
+
const display = document.getElementById('upstream-key-display').textContent;
|
|
692
|
+
copyText(display, btn);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function handleCreateUpstreamKey() {
|
|
696
|
+
try {
|
|
697
|
+
const resp = await fetch('/api/upstream-keys', { method: 'POST' });
|
|
698
|
+
const data = await resp.json();
|
|
699
|
+
if (data.key) {
|
|
700
|
+
document.getElementById('newUpstreamKeyAlert').style.display = 'block';
|
|
701
|
+
document.getElementById('newUpstreamKeyValue').textContent = data.key;
|
|
702
|
+
loadUpstreamKeys();
|
|
703
|
+
}
|
|
704
|
+
} catch (e) {
|
|
705
|
+
alert('Failed to create key: ' + e.message);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function handleDeleteUpstreamKey(maskedKey) {
|
|
710
|
+
if (!confirm('Delete this copilot-api key?')) return;
|
|
711
|
+
const prefix = maskedKey.split('...')[0];
|
|
712
|
+
try {
|
|
713
|
+
await fetch('/api/upstream-keys/' + encodeURIComponent(prefix), { method: 'DELETE' });
|
|
714
|
+
document.getElementById('newUpstreamKeyAlert').style.display = 'none';
|
|
715
|
+
loadUpstreamKeys();
|
|
716
|
+
} catch (e) {
|
|
717
|
+
alert('Failed to delete key: ' + e.message);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
loadUpstreamKeys();
|
|
722
|
+
|
|
620
723
|
/* ── Tab 1: API Key Management ─────────────────────────────── */
|
|
621
724
|
let authConfig = { requireApiKey: false, keys: [] };
|
|
622
725
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-cursor-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Proxy that bridges GitHub Copilot API to Cursor IDE — translates Anthropic format, bridges Responses API for GPT 5.x, and more",
|
|
5
5
|
"bin": {
|
|
6
6
|
"copilot-cursor-proxy": "bin/cli.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "bun build start.ts proxy-router.ts anthropic-transforms.ts responses-bridge.ts responses-converters.ts stream-proxy.ts debug-logger.ts auth-config.ts --outdir dist --target node",
|
|
15
|
+
"build": "bun build start.ts proxy-router.ts anthropic-transforms.ts responses-bridge.ts responses-converters.ts stream-proxy.ts debug-logger.ts auth-config.ts upstream-auth.ts --outdir dist --target node",
|
|
16
16
|
"dev": "bun run start.ts",
|
|
17
17
|
"start": "bun dist/start.js"
|
|
18
18
|
},
|
package/proxy-router.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createStreamProxy } from './stream-proxy';
|
|
|
4
4
|
import { logIncomingRequest, logTransformedRequest } from './debug-logger';
|
|
5
5
|
import { addRequestLog, getNextRequestId, getUsageStats, flushToDisk, type RequestLog } from './usage-db';
|
|
6
6
|
import { loadAuthConfig, saveAuthConfig, generateApiKey, validateApiKey } from './auth-config';
|
|
7
|
+
import { getUpstreamAuthHeader, getUpstreamApiKeys, createUpstreamApiKey, deleteUpstreamApiKey } from './upstream-auth';
|
|
7
8
|
|
|
8
9
|
// ── Console capture for SSE streaming ─────────────────────────────────────────
|
|
9
10
|
interface ConsoleLine {
|
|
@@ -169,11 +170,40 @@ Bun.serve({
|
|
|
169
170
|
return Response.json({ ok: true }, { headers: corsHeaders });
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
// ── Upstream (copilot-api) key management ────────────────────────
|
|
174
|
+
if (url.pathname === "/api/upstream-keys" && req.method === "GET") {
|
|
175
|
+
const keys = getUpstreamApiKeys();
|
|
176
|
+
const masked = keys.map(k => k.slice(0, 14) + '...' + k.slice(-4));
|
|
177
|
+
return Response.json({ keys: masked, count: keys.length }, { headers: corsHeaders });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (url.pathname === "/api/upstream-keys" && req.method === "POST") {
|
|
181
|
+
try {
|
|
182
|
+
const newKey = createUpstreamApiKey();
|
|
183
|
+
return Response.json({ key: newKey }, { headers: corsHeaders });
|
|
184
|
+
} catch (e: any) {
|
|
185
|
+
return Response.json({ error: e?.message || 'Failed to create key' }, { status: 500, headers: corsHeaders });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (url.pathname.startsWith("/api/upstream-keys/") && req.method === "DELETE") {
|
|
190
|
+
const keyPrefix = decodeURIComponent(url.pathname.split('/').pop() || '');
|
|
191
|
+
const keys = getUpstreamApiKeys();
|
|
192
|
+
const match = keys.find(k => k.startsWith(keyPrefix) || k.endsWith(keyPrefix));
|
|
193
|
+
if (match) {
|
|
194
|
+
deleteUpstreamApiKey(match);
|
|
195
|
+
return Response.json({ ok: true }, { headers: corsHeaders });
|
|
196
|
+
}
|
|
197
|
+
return Response.json({ error: 'Key not found' }, { status: 404, headers: corsHeaders });
|
|
198
|
+
}
|
|
199
|
+
|
|
172
200
|
// ── Dashboard API: model list (bypasses API key auth) ──────────────
|
|
173
201
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
174
202
|
try {
|
|
175
203
|
const modelsUrl = new URL('/v1/models', TARGET_URL);
|
|
176
|
-
const response = await fetch(modelsUrl.toString()
|
|
204
|
+
const response = await fetch(modelsUrl.toString(), {
|
|
205
|
+
headers: { 'Authorization': getUpstreamAuthHeader() },
|
|
206
|
+
});
|
|
177
207
|
const data = await response.json();
|
|
178
208
|
if (data.data && Array.isArray(data.data)) {
|
|
179
209
|
data.data = data.data.map((model: any) => ({
|
|
@@ -243,7 +273,7 @@ Bun.serve({
|
|
|
243
273
|
|
|
244
274
|
const headers = new Headers(req.headers);
|
|
245
275
|
headers.set("host", targetUrl.host);
|
|
246
|
-
headers.
|
|
276
|
+
headers.set("authorization", getUpstreamAuthHeader());
|
|
247
277
|
|
|
248
278
|
const needsResponsesAPI = targetModel.match(/^gpt-5\.[2-9]|^gpt-5\.\d+-codex|^o[1-9]|^goldeneye/i);
|
|
249
279
|
|
|
@@ -344,7 +374,7 @@ Bun.serve({
|
|
|
344
374
|
if (req.method === "GET" && url.pathname.includes("/models")) {
|
|
345
375
|
const headers = new Headers(req.headers);
|
|
346
376
|
headers.set("host", targetUrl.host);
|
|
347
|
-
headers.
|
|
377
|
+
headers.set("authorization", getUpstreamAuthHeader());
|
|
348
378
|
const response = await fetch(targetUrl.toString(), { method: "GET", headers: headers });
|
|
349
379
|
const data = await response.json();
|
|
350
380
|
|
|
@@ -363,7 +393,7 @@ Bun.serve({
|
|
|
363
393
|
|
|
364
394
|
const headers = new Headers(req.headers);
|
|
365
395
|
headers.set("host", targetUrl.host);
|
|
366
|
-
headers.
|
|
396
|
+
headers.set("authorization", getUpstreamAuthHeader());
|
|
367
397
|
const response = await fetch(targetUrl.toString(), {
|
|
368
398
|
method: req.method,
|
|
369
399
|
headers: headers,
|
package/responses-bridge.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { convertResponsesSyncToChatCompletions, convertResponsesStreamToChatCompletions } from './responses-converters';
|
|
2
|
+
import { getUpstreamAuthHeader } from './upstream-auth';
|
|
2
3
|
|
|
3
4
|
export interface BridgeResult {
|
|
4
5
|
response: Response;
|
|
@@ -101,7 +102,7 @@ export async function handleResponsesAPIBridge(json: any, req: Request, chatId:
|
|
|
101
102
|
headers.set("host", responsesUrl.host);
|
|
102
103
|
headers.set("content-type", "application/json");
|
|
103
104
|
headers.set("content-length", String(new TextEncoder().encode(responsesBody).length));
|
|
104
|
-
headers.
|
|
105
|
+
headers.set("authorization", getUpstreamAuthHeader());
|
|
105
106
|
|
|
106
107
|
const response = await fetch(responsesUrl.toString(), {
|
|
107
108
|
method: "POST",
|
package/start.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { spawn, sleep } from 'bun';
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
|
+
import { getUpstreamAuthHeader } from './upstream-auth';
|
|
9
10
|
|
|
10
11
|
const COPILOT_API_PORT = 4141;
|
|
11
12
|
const PROXY_PORT = 4142;
|
|
@@ -30,7 +31,9 @@ async function waitForPort(port: number, timeoutMs = 30000): Promise<boolean> {
|
|
|
30
31
|
const start = Date.now();
|
|
31
32
|
while (Date.now() - start < timeoutMs) {
|
|
32
33
|
try {
|
|
33
|
-
const resp = await fetch(`http://localhost:${port}/v1/models
|
|
34
|
+
const resp = await fetch(`http://localhost:${port}/v1/models`, {
|
|
35
|
+
headers: { 'Authorization': getUpstreamAuthHeader() },
|
|
36
|
+
});
|
|
34
37
|
if (resp.ok) return true;
|
|
35
38
|
} catch {}
|
|
36
39
|
await sleep(500);
|
package/upstream-auth.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DATA_DIR = join(homedir(), '.local', 'share', 'copilot-api');
|
|
7
|
+
|
|
8
|
+
interface CopilotApiConfig {
|
|
9
|
+
auth?: { apiKeys?: string[] };
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let cachedKey: string | null = null;
|
|
14
|
+
|
|
15
|
+
function getConfigPath(): string {
|
|
16
|
+
const dataDir = process.env.COPILOT_API_HOME || DEFAULT_DATA_DIR;
|
|
17
|
+
return join(dataDir, 'config.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadUpstreamConfig(): CopilotApiConfig {
|
|
21
|
+
const configPath = getConfigPath();
|
|
22
|
+
try {
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveUpstreamConfig(config: CopilotApiConfig): void {
|
|
31
|
+
const configPath = getConfigPath();
|
|
32
|
+
const dir = join(configPath, '..');
|
|
33
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
34
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
35
|
+
cachedKey = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getUpstreamApiKeys(): string[] {
|
|
39
|
+
const config = loadUpstreamConfig();
|
|
40
|
+
return Array.isArray(config.auth?.apiKeys) ? config.auth!.apiKeys! : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getUpstreamApiKey(): string {
|
|
44
|
+
if (cachedKey) return cachedKey;
|
|
45
|
+
|
|
46
|
+
const keys = getUpstreamApiKeys();
|
|
47
|
+
if (keys.length > 0) {
|
|
48
|
+
cachedKey = keys[0];
|
|
49
|
+
return cachedKey;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cachedKey = 'dummy';
|
|
53
|
+
return cachedKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getUpstreamAuthHeader(): string {
|
|
57
|
+
return `Bearer ${getUpstreamApiKey()}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createUpstreamApiKey(): string {
|
|
61
|
+
const config = loadUpstreamConfig();
|
|
62
|
+
if (!config.auth) config.auth = {};
|
|
63
|
+
if (!Array.isArray(config.auth.apiKeys)) config.auth.apiKeys = [];
|
|
64
|
+
|
|
65
|
+
const newKey = 'sk-copilot-' + randomBytes(16).toString('base64url');
|
|
66
|
+
config.auth.apiKeys.push(newKey);
|
|
67
|
+
saveUpstreamConfig(config);
|
|
68
|
+
return newKey;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteUpstreamApiKey(key: string): boolean {
|
|
72
|
+
const config = loadUpstreamConfig();
|
|
73
|
+
const keys = config.auth?.apiKeys;
|
|
74
|
+
if (!Array.isArray(keys)) return false;
|
|
75
|
+
|
|
76
|
+
const idx = keys.indexOf(key);
|
|
77
|
+
if (idx === -1) return false;
|
|
78
|
+
|
|
79
|
+
keys.splice(idx, 1);
|
|
80
|
+
saveUpstreamConfig(config);
|
|
81
|
+
return true;
|
|
82
|
+
}
|