From edecb8ea5a0a9012f43b6478ff4468012ab2178f Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:56:45 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feat/ap?= =?UTF-8?q?i-key-support`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @ZiuChen. * https://github.com/ericc-ch/copilot-api/pull/144#issuecomment-3510278299 The following files were modified: * `src/lib/api-key-auth.ts` * `src/start.ts` --- src/lib/api-key-auth.ts | 68 +++++++++++++++++++++++++++++++++++++++++ src/start.ts | 43 +++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/lib/api-key-auth.ts diff --git a/src/lib/api-key-auth.ts b/src/lib/api-key-auth.ts new file mode 100644 index 0000000..824d81e --- /dev/null +++ b/src/lib/api-key-auth.ts @@ -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() +} \ No newline at end of file diff --git a/src/start.ts b/src/start.ts index 14abbbd..b95a38d 100644 --- a/src/start.ts +++ b/src/start.ts @@ -25,8 +25,30 @@ interface RunServerOptions { claudeCode: boolean showToken: boolean proxyEnv: boolean + apiKeys?: Array } +/** + * 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 { if (options.proxyEnv) { initProxyFromEnv() @@ -46,6 +68,13 @@ export async function runServer(options: RunServerOptions): Promise { 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() @@ -184,6 +213,10 @@ 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"] @@ -191,6 +224,13 @@ export const start = defineCommand({ // 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 | undefined + if (apiKeyRaw) { + apiKeys = Array.isArray(apiKeyRaw) ? apiKeyRaw : [apiKeyRaw] + } + return runServer({ port: Number.parseInt(args.port, 10), verbose: args.verbose, @@ -202,6 +242,7 @@ export const start = defineCommand({ claudeCode: args["claude-code"], showToken: args["show-token"], proxyEnv: args["proxy-env"], + apiKeys, }) }, -}) +}) \ No newline at end of file