Skip to content

Commit 72e9cc8

Browse files
kubaerorberileviZiuChenCopilot
committed
feat: add --host option and API key authentication
- Add --host/-H option to bind server to specific interface (PR ericc-ch#157) - Add --api-key/-k option for securing proxy endpoints (PR ericc-ch#144) - Support both OpenAI Bearer and Anthropic x-api-key formats Co-authored-by: berilevi <berilevi@users.noreply.github.com> Co-authored-by: ZiuChen <ZiuChen@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 768f573 commit 72e9cc8

File tree

4 files changed

+126
-1
lines changed

4 files changed

+126
-1
lines changed

src/lib/api-key-auth.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* API Key Authentication Middleware
3+
* PR #144: Support specifying multiple API keys (@ZiuChen)
4+
*
5+
* This middleware provides API key authentication for securing proxy endpoints.
6+
* It supports both OpenAI (Authorization: Bearer <token>) and Anthropic (x-api-key: <token>) formats.
7+
*/
8+
9+
import type { Context, Next } from "hono"
10+
11+
import { state } from "./state"
12+
13+
/**
14+
* Extracts the API key from the request headers.
15+
* Supports both OpenAI Bearer token and Anthropic x-api-key formats.
16+
*/
17+
function extractApiKey(c: Context): string | undefined {
18+
// Try OpenAI format: Authorization: Bearer <token>
19+
const authHeader = c.req.header("Authorization")
20+
if (authHeader?.startsWith("Bearer ")) {
21+
return authHeader.slice(7)
22+
}
23+
24+
// Try Anthropic format: x-api-key: <token>
25+
const xApiKey = c.req.header("x-api-key")
26+
if (xApiKey) {
27+
return xApiKey
28+
}
29+
30+
return undefined
31+
}
32+
33+
/**
34+
* Middleware that validates API key authentication.
35+
* If no API keys are configured, all requests are allowed.
36+
* If API keys are configured, requests must provide a valid key.
37+
*/
38+
export async function apiKeyAuth(
39+
c: Context,
40+
next: Next,
41+
): Promise<Response | undefined> {
42+
// If no API keys are configured, allow all requests
43+
if (!state.apiKeys || state.apiKeys.length === 0) {
44+
return next()
45+
}
46+
47+
const providedKey = extractApiKey(c)
48+
49+
// No key provided
50+
if (!providedKey) {
51+
return c.json(
52+
{
53+
error: {
54+
message:
55+
"API key required. Provide via 'Authorization: Bearer <key>' or 'x-api-key: <key>' header.",
56+
type: "authentication_error",
57+
},
58+
},
59+
401,
60+
)
61+
}
62+
63+
// Invalid key
64+
if (!state.apiKeys.includes(providedKey)) {
65+
return c.json(
66+
{
67+
error: {
68+
message: "Invalid API key",
69+
type: "authentication_error",
70+
},
71+
},
72+
401,
73+
)
74+
}
75+
76+
// Valid key, proceed
77+
return next()
78+
}

src/lib/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export interface State {
1515
// Rate limiting configuration
1616
rateLimitSeconds?: number
1717
lastRequestTimestamp?: number
18+
19+
// PR #144: API key authentication (@ZiuChen)
20+
apiKeys?: Array<string>
1821
}
1922

2023
export const state: State = {

src/server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Hono } from "hono"
22
import { cors } from "hono/cors"
33
import { logger } from "hono/logger"
44

5+
import { apiKeyAuth } from "./lib/api-key-auth"
56
import { completionRoutes } from "./routes/chat-completions/route"
67
import { embeddingRoutes } from "./routes/embeddings/route"
78
import { messageRoutes } from "./routes/messages/route"
@@ -14,8 +15,15 @@ export const server = new Hono()
1415
server.use(logger())
1516
server.use(cors())
1617

18+
// Root endpoint is public (no API key required)
1719
server.get("/", (c) => c.text("Server running"))
1820

21+
// PR #144: Apply API key auth to all API routes (@ZiuChen)
22+
server.use("/chat/*", apiKeyAuth)
23+
server.use("/models/*", apiKeyAuth)
24+
server.use("/embeddings/*", apiKeyAuth)
25+
server.use("/v1/*", apiKeyAuth)
26+
1927
server.route("/chat/completions", completionRoutes)
2028
server.route("/models", modelRoutes)
2129
server.route("/embeddings", embeddingRoutes)

src/start.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { server } from "./server"
1616

1717
interface RunServerOptions {
1818
port: number
19+
host?: string
1920
verbose: boolean
2021
accountType: string
2122
manual: boolean
@@ -25,13 +26,22 @@ interface RunServerOptions {
2526
claudeCode: boolean
2627
showToken: boolean
2728
proxyEnv: boolean
29+
apiKeys?: Array<string>
2830
}
2931

3032
export async function runServer(options: RunServerOptions): Promise<void> {
3133
if (options.proxyEnv) {
3234
initProxyFromEnv()
3335
}
3436

37+
// PR #144: Configure API keys (@ZiuChen)
38+
if (options.apiKeys && options.apiKeys.length > 0) {
39+
state.apiKeys = options.apiKeys
40+
consola.info(
41+
`API key authentication enabled with ${options.apiKeys.length} key(s)`,
42+
)
43+
}
44+
3545
if (options.verbose) {
3646
consola.level = 5
3747
consola.info("Verbose logging enabled")
@@ -64,7 +74,9 @@ export async function runServer(options: RunServerOptions): Promise<void> {
6474
`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`,
6575
)
6676

67-
const serverUrl = `http://localhost:${options.port}`
77+
// PR #157: Build server URL with host option (@berilevi)
78+
const host = options.host ?? "localhost"
79+
const serverUrl = `http://${host}:${options.port}`
6880

6981
if (options.claudeCode) {
7082
invariant(state.models, "Models should be loaded by now")
@@ -117,6 +129,7 @@ export async function runServer(options: RunServerOptions): Promise<void> {
117129
serve({
118130
fetch: server.fetch as ServerHandler,
119131
port: options.port,
132+
hostname: options.host,
120133
})
121134
}
122135

@@ -132,6 +145,13 @@ export const start = defineCommand({
132145
default: "4141",
133146
description: "Port to listen on",
134147
},
148+
// PR #157: Add --host option (@berilevi)
149+
host: {
150+
alias: "H",
151+
type: "string",
152+
description:
153+
"Host/interface to bind to (e.g., 127.0.0.1 for localhost only)",
154+
},
135155
verbose: {
136156
alias: "v",
137157
type: "boolean",
@@ -184,15 +204,30 @@ export const start = defineCommand({
184204
default: false,
185205
description: "Initialize proxy from environment variables",
186206
},
207+
// PR #144: Support specifying multiple API keys (@ZiuChen)
208+
"api-key": {
209+
alias: "k",
210+
type: "string",
211+
description:
212+
"API key(s) for authentication. Can be specified multiple times for multiple keys",
213+
},
187214
},
188215
run({ args }) {
189216
const rateLimitRaw = args["rate-limit"]
190217
const rateLimit =
191218
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
192219
rateLimitRaw === undefined ? undefined : Number.parseInt(rateLimitRaw, 10)
193220

221+
// Parse API keys (can be a single string or array)
222+
const apiKeyArg = args["api-key"]
223+
let apiKeys: Array<string> | undefined
224+
if (apiKeyArg) {
225+
apiKeys = Array.isArray(apiKeyArg) ? apiKeyArg : [apiKeyArg]
226+
}
227+
194228
return runServer({
195229
port: Number.parseInt(args.port, 10),
230+
host: args.host,
196231
verbose: args.verbose,
197232
accountType: args["account-type"],
198233
manual: args.manual,
@@ -202,6 +237,7 @@ export const start = defineCommand({
202237
claudeCode: args["claude-code"],
203238
showToken: args["show-token"],
204239
proxyEnv: args["proxy-env"],
240+
apiKeys,
205241
})
206242
},
207243
})

0 commit comments

Comments
 (0)