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
68 changes: 68 additions & 0 deletions src/lib/api-key-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Context, MiddlewareHandler } from "hono"

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

import { state } from "./state"

/**
* Retrieve an API key from the incoming request.
*
* Checks common locations where clients supply keys (Authorization Bearer header, `x-api-key` header, or `apiKey` query parameter) and returns the first one found.
*
* @returns The extracted API key, or `undefined` if no key is present.
*/
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
}

// Fallback: query parameter, for extra compatibility of `/usage` or `/token` route
const queryKey = c.req.query("apiKey")
if (queryKey) {
return queryKey
}

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:
"API key required. Please provide a valid API key in the Authorization header (Bearer token) or x-api-key header.",
})
}

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

if (!isValidKey) {
throw new HTTPException(401, {
message: "Invalid API key. Please provide a valid API key.",
})
}

// Key is valid, continue with the request
await next()
}
43 changes: 42 additions & 1 deletion src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,30 @@ interface RunServerOptions {
claudeCode: boolean
showToken: boolean
proxyEnv: boolean
apiKeys?: Array<string>
}

/**
* Start and configure the Copilot API server according to the provided options.
*
* Configures proxy and logging, initializes global state and credentials, ensures
* required paths and model data are cached, optionally generates a Claude Code
* launch command (and attempts to copy it to the clipboard), prints a usage
* viewer URL, and begins serving HTTP requests on the specified port.
*
* @param options - Server startup options:
* - port: Port number to listen on
* - verbose: Enable verbose logging
* - accountType: Account plan to use ("individual", "business", "enterprise")
* - manual: Require manual approval for requests
* - rateLimit: Seconds to wait between requests (optional)
* - rateLimitWait: Wait instead of erroring when rate limit is hit
* - githubToken: GitHub token to use (optional; if omitted a token setup prompt may run)
* - claudeCode: Generate a Claude Code environment launch command
* - showToken: Expose GitHub/Copilot tokens in responses for debugging
* - proxyEnv: Initialize proxy settings from environment variables
* - apiKeys: Optional list of API keys to enable API key authentication
*/
export async function runServer(options: RunServerOptions): Promise<void> {
if (options.proxyEnv) {
initProxyFromEnv()
Expand All @@ -46,6 +68,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 +213,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 +242,7 @@ export const start = defineCommand({
claudeCode: args["claude-code"],
showToken: args["show-token"],
proxyEnv: args["proxy-env"],
apiKeys,
})
},
})
})
Loading