Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ The following command line options are available for the `start` command:
| --claude-code | Generate a command to launch Claude Code with Copilot API config | false | -c |
| --show-token | Show GitHub and Copilot tokens on fetch and refresh | false | none |
| --proxy-env | Initialize proxy from environment variables | false | none |
| --api-key | API keys for authentication. Can be specified multiple times | none | none |

### Auth Command Options

Expand Down Expand Up @@ -209,6 +210,41 @@ New endpoints for monitoring your Copilot usage and quotas.
| `GET /usage` | `GET` | Get detailed Copilot usage statistics and quota information. |
| `GET /token` | `GET` | Get the current Copilot token being used by the API. |

## API Key Authentication

The proxy supports API key authentication to restrict access to the endpoints. When API keys are configured, all API endpoints require authentication.

### Authentication Methods

The proxy supports both OpenAI and Anthropic authentication formats:

- **OpenAI format**: Include the API key in the `Authorization` header with `Bearer` prefix:
```bash
curl -H "Authorization: Bearer your_api_key_here" http://localhost:4141/v1/models
```

- **Anthropic format**: Include the API key in the `x-api-key` header:
```bash
curl -H "x-api-key: your_api_key_here" http://localhost:4141/v1/messages
```

### Configuration

Use the `--api-key` flag to enable API key authentication. You can specify multiple keys for different clients:

```bash
# Single API key
npx copilot-api@latest start --api-key your_secret_key

# Multiple API keys
npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3
```

When API keys are configured:
- All API endpoints (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/messages`, `/usage`, `/token`) require authentication
- Requests without valid API keys will receive a 401 Unauthorized response
- The root endpoint `/` remains accessible without authentication
Comment on lines +213 to +246
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Document usage viewer behavior with API key authentication.

The API Key Authentication section is well-written and comprehensive. However, there's a documentation gap: the Usage Viewer section (lines 302-322) doesn't explain how to access the dashboard when API key authentication is enabled. Users may be confused about whether they need to provide an API key in the URL or if the endpoint remains accessible.

Consider adding a note in either the API Key Authentication section or the Usage Viewer section explaining how authentication works with the browser-based dashboard (e.g., whether the /usage endpoint requires authentication when accessed directly vs. through the dashboard).

πŸ€– Prompt for AI Agents
In README.md around lines 213-246, add a short note clarifying how the Usage
Viewer/dashboard is affected when API key authentication is enabled: inspect the
server implementation to determine whether the /usage endpoint and the browser
dashboard require the API key, and then document the exact way to authenticate
when using the dashboard (e.g., which header/query param/cookie or UI login flow
the dashboard accepts), including an example of how to provide the key from a
browser (or mention browser extensions/tools if direct header setting is not
supported).


## Example Usage

Using with npx:
Expand Down Expand Up @@ -238,6 +274,12 @@ npx copilot-api@latest start --rate-limit 30 --wait
# Provide GitHub token directly
npx copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE

# Enable API key authentication with a single key
npx copilot-api@latest start --api-key your_secret_key_here

# Enable API key authentication with multiple keys
npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3

# Run only the auth flow
npx copilot-api@latest auth

Expand Down
61 changes: 61 additions & 0 deletions src/lib/api-key-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Context, MiddlewareHandler } from "hono"

import { HTTPException } from "hono/http-exception"

import { state } from "./state"
import { constantTimeEqual } from "./utils"

/**
* Extract API key from request headers
* Supports both OpenAI format (Authorization: Bearer token) and Anthropic format (x-api-key: token)
*/
function extractApiKey(c: Context): string | undefined {
// OpenAI format: Authorization header with Bearer prefix
const authHeader = c.req.header("authorization")
if (authHeader?.startsWith("Bearer ")) {
return authHeader.slice(7) // Remove 'Bearer ' prefix
}

// Anthropic format: x-api-key header
const anthropicKey = c.req.header("x-api-key")
if (anthropicKey) {
return anthropicKey
}

return undefined
}

/**
* API key authentication middleware
* Validates that the request contains a valid API key if API keys are configured
*/
export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => {
// If no API keys are configured, skip authentication
if (!state.apiKeys || state.apiKeys.length === 0) {
await next()
return
}

const providedKey = extractApiKey(c)

// If no API key is provided, return 401
if (!providedKey) {
throw new HTTPException(401, {
message: "Missing API key",
})
}

// Check if the provided key matches any of the configured keys
const isValidKey = state.apiKeys.some((key) =>
constantTimeEqual(key, providedKey),
)

if (!isValidKey) {
throw new HTTPException(401, {
message: "Invalid API key",
})
}

// Key is valid, continue with the request
await next()
}
3 changes: 3 additions & 0 deletions src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export interface State {
// Rate limiting configuration
rateLimitSeconds?: number
lastRequestTimestamp?: number

// API key validation
apiKeys?: Array<string>
}

export const state: State = {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ export const cacheVSCodeVersion = async () => {

consola.info(`Using VSCode version: ${response}`)
}

export const constantTimeEqual = (a: string, b: string): boolean => {
if (a.length !== b.length) {
return false
}
let result = 0
for (let i = 0; i < a.length; i++) {
result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0)
}
return result === 0
}
11 changes: 11 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from "hono"
import { cors } from "hono/cors"
import { logger } from "hono/logger"

import { apiKeyAuthMiddleware } from "./lib/api-key-auth"
import { completionRoutes } from "./routes/chat-completions/route"
import { embeddingRoutes } from "./routes/embeddings/route"
import { messageRoutes } from "./routes/messages/route"
Expand All @@ -14,6 +15,16 @@ export const server = new Hono()
server.use(logger())
server.use(cors())

server.use("/chat/completions", apiKeyAuthMiddleware)
server.use("/models", apiKeyAuthMiddleware)
server.use("/embeddings", apiKeyAuthMiddleware)
server.use("/usage", apiKeyAuthMiddleware)
server.use("/token", apiKeyAuthMiddleware)
server.use("/v1/chat/completions", apiKeyAuthMiddleware)
server.use("/v1/models", apiKeyAuthMiddleware)
server.use("/v1/embeddings", apiKeyAuthMiddleware)
server.use("/v1/messages", apiKeyAuthMiddleware)

server.get("/", (c) => c.text("Server running"))

server.route("/chat/completions", completionRoutes)
Expand Down
20 changes: 20 additions & 0 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface RunServerOptions {
claudeCode: boolean
showToken: boolean
proxyEnv: boolean
apiKeys?: Array<string>
}

export async function runServer(options: RunServerOptions): Promise<void> {
Expand All @@ -46,6 +47,13 @@ export async function runServer(options: RunServerOptions): Promise<void> {
state.rateLimitSeconds = options.rateLimit
state.rateLimitWait = options.rateLimitWait
state.showToken = options.showToken
state.apiKeys = options.apiKeys

if (state.apiKeys && state.apiKeys.length > 0) {
consola.info(
`API key authentication enabled with ${state.apiKeys.length} key(s)`,
)
}

await ensurePaths()
await cacheVSCodeVersion()
Expand Down Expand Up @@ -184,13 +192,24 @@ export const start = defineCommand({
default: false,
description: "Initialize proxy from environment variables",
},
"api-key": {
type: "string",
description: "API keys for authentication",
},
},
run({ args }) {
const rateLimitRaw = args["rate-limit"]
const rateLimit =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
rateLimitRaw === undefined ? undefined : Number.parseInt(rateLimitRaw, 10)

// Handle multiple API keys - citty may pass a string or array
const apiKeyRaw = args["api-key"]
let apiKeys: Array<string> | undefined
if (apiKeyRaw) {
apiKeys = Array.isArray(apiKeyRaw) ? apiKeyRaw : [apiKeyRaw]
}

return runServer({
port: Number.parseInt(args.port, 10),
verbose: args.verbose,
Expand All @@ -202,6 +221,7 @@ export const start = defineCommand({
claudeCode: args["claude-code"],
showToken: args["show-token"],
proxyEnv: args["proxy-env"],
apiKeys,
})
},
})