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 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">dummy</div>
380
- <button class="copy-btn" onclick="copyText('dummy',this)">Copy</button>
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.1.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.delete("authorization"); // Don't leak proxy API keys upstream
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.delete("authorization"); // Don't leak proxy API keys upstream
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.delete("authorization"); // Don't leak proxy API keys upstream
396
+ headers.set("authorization", getUpstreamAuthHeader());
367
397
  const response = await fetch(targetUrl.toString(), {
368
398
  method: req.method,
369
399
  headers: headers,
@@ -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.delete("authorization"); // Don't leak proxy API keys upstream
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);
@@ -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
+ }