Skip to content

Commit edecb8e

Browse files
📝 Add docstrings to feat/api-key-support
Docstrings generation was requested by @ZiuChen. * #144 (comment) The following files were modified: * `src/lib/api-key-auth.ts` * `src/start.ts`
1 parent 0ea08fe commit edecb8e

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

src/lib/api-key-auth.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Context, MiddlewareHandler } from "hono"
2+
3+
import { HTTPException } from "hono/http-exception"
4+
5+
import { state } from "./state"
6+
7+
/**
8+
* Retrieve an API key from the incoming request.
9+
*
10+
* Checks common locations where clients supply keys (Authorization Bearer header, `x-api-key` header, or `apiKey` query parameter) and returns the first one found.
11+
*
12+
* @returns The extracted API key, or `undefined` if no key is present.
13+
*/
14+
function extractApiKey(c: Context): string | undefined {
15+
// OpenAI format: Authorization header with Bearer prefix
16+
const authHeader = c.req.header("authorization")
17+
if (authHeader?.startsWith("Bearer ")) {
18+
return authHeader.slice(7) // Remove 'Bearer ' prefix
19+
}
20+
21+
// Anthropic format: x-api-key header
22+
const anthropicKey = c.req.header("x-api-key")
23+
if (anthropicKey) {
24+
return anthropicKey
25+
}
26+
27+
// Fallback: query parameter, for extra compatibility of `/usage` or `/token` route
28+
const queryKey = c.req.query("apiKey")
29+
if (queryKey) {
30+
return queryKey
31+
}
32+
33+
return undefined
34+
}
35+
36+
/**
37+
* API key authentication middleware
38+
* Validates that the request contains a valid API key if API keys are configured
39+
*/
40+
export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => {
41+
// If no API keys are configured, skip authentication
42+
if (!state.apiKeys || state.apiKeys.length === 0) {
43+
await next()
44+
return
45+
}
46+
47+
const providedKey = extractApiKey(c)
48+
49+
// If no API key is provided, return 401
50+
if (!providedKey) {
51+
throw new HTTPException(401, {
52+
message:
53+
"API key required. Please provide a valid API key in the Authorization header (Bearer token) or x-api-key header.",
54+
})
55+
}
56+
57+
// Check if the provided key matches any of the configured keys
58+
const isValidKey = state.apiKeys.includes(providedKey)
59+
60+
if (!isValidKey) {
61+
throw new HTTPException(401, {
62+
message: "Invalid API key. Please provide a valid API key.",
63+
})
64+
}
65+
66+
// Key is valid, continue with the request
67+
await next()
68+
}

src/start.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,30 @@ interface RunServerOptions {
2525
claudeCode: boolean
2626
showToken: boolean
2727
proxyEnv: boolean
28+
apiKeys?: Array<string>
2829
}
2930

31+
/**
32+
* Start and configure the Copilot API server according to the provided options.
33+
*
34+
* Configures proxy and logging, initializes global state and credentials, ensures
35+
* required paths and model data are cached, optionally generates a Claude Code
36+
* launch command (and attempts to copy it to the clipboard), prints a usage
37+
* viewer URL, and begins serving HTTP requests on the specified port.
38+
*
39+
* @param options - Server startup options:
40+
* - port: Port number to listen on
41+
* - verbose: Enable verbose logging
42+
* - accountType: Account plan to use ("individual", "business", "enterprise")
43+
* - manual: Require manual approval for requests
44+
* - rateLimit: Seconds to wait between requests (optional)
45+
* - rateLimitWait: Wait instead of erroring when rate limit is hit
46+
* - githubToken: GitHub token to use (optional; if omitted a token setup prompt may run)
47+
* - claudeCode: Generate a Claude Code environment launch command
48+
* - showToken: Expose GitHub/Copilot tokens in responses for debugging
49+
* - proxyEnv: Initialize proxy settings from environment variables
50+
* - apiKeys: Optional list of API keys to enable API key authentication
51+
*/
3052
export async function runServer(options: RunServerOptions): Promise<void> {
3153
if (options.proxyEnv) {
3254
initProxyFromEnv()
@@ -46,6 +68,13 @@ export async function runServer(options: RunServerOptions): Promise<void> {
4668
state.rateLimitSeconds = options.rateLimit
4769
state.rateLimitWait = options.rateLimitWait
4870
state.showToken = options.showToken
71+
state.apiKeys = options.apiKeys
72+
73+
if (state.apiKeys && state.apiKeys.length > 0) {
74+
consola.info(
75+
`API key authentication enabled with ${state.apiKeys.length} key(s)`,
76+
)
77+
}
4978

5079
await ensurePaths()
5180
await cacheVSCodeVersion()
@@ -184,13 +213,24 @@ export const start = defineCommand({
184213
default: false,
185214
description: "Initialize proxy from environment variables",
186215
},
216+
"api-key": {
217+
type: "string",
218+
description: "API keys for authentication",
219+
},
187220
},
188221
run({ args }) {
189222
const rateLimitRaw = args["rate-limit"]
190223
const rateLimit =
191224
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
192225
rateLimitRaw === undefined ? undefined : Number.parseInt(rateLimitRaw, 10)
193226

227+
// Handle multiple API keys - citty may pass a string or array
228+
const apiKeyRaw = args["api-key"]
229+
let apiKeys: Array<string> | undefined
230+
if (apiKeyRaw) {
231+
apiKeys = Array.isArray(apiKeyRaw) ? apiKeyRaw : [apiKeyRaw]
232+
}
233+
194234
return runServer({
195235
port: Number.parseInt(args.port, 10),
196236
verbose: args.verbose,
@@ -202,6 +242,7 @@ export const start = defineCommand({
202242
claudeCode: args["claude-code"],
203243
showToken: args["show-token"],
204244
proxyEnv: args["proxy-env"],
245+
apiKeys,
205246
})
206247
},
207-
})
248+
})

0 commit comments

Comments
 (0)