feat: add API key authentication for AI proxy endpoints
- Add API_KEY config (env var) to all entry points - requireAuth helper checks Authorization: Bearer or x-api-key header - Auth applied to /v1/chat/completions, /v1/messages, /v1/models - When API_KEY is empty/unset, auth is disabled (backward compatible) - Update wrangler.toml with API_KEY variable documentation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2be57c0838
commit
c0a02279bd
@@ -39,6 +39,21 @@ const RELAY_TIMEOUT_MS = Number.parseInt(
|
|||||||
const SERVER_START_TIME = Date.now();
|
const SERVER_START_TIME = Date.now();
|
||||||
const RELAY_VERSION = "1.0.0";
|
const RELAY_VERSION = "1.0.0";
|
||||||
|
|
||||||
|
// ─── API Key Authentication ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const API_KEY = process.env.API_KEY ?? "";
|
||||||
|
|
||||||
|
function requireAuth(req: Request): Response | null {
|
||||||
|
if (!API_KEY) return null;
|
||||||
|
const header = req.headers.get("authorization") ?? req.headers.get("x-api-key") ?? "";
|
||||||
|
const key = header.replace(/^Bearer\s+/i, "").trim();
|
||||||
|
if (key === API_KEY) return null;
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: { message: "Unauthorized", type: "auth_error" } }),
|
||||||
|
{ status: 401, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Middleware instances (singletons — persist across warm invocations) ─────────
|
// ─── Middleware instances (singletons — persist across warm invocations) ─────────
|
||||||
|
|
||||||
const rateLimiter = createRateLimiter({
|
const rateLimiter = createRateLimiter({
|
||||||
@@ -400,6 +415,8 @@ export default {
|
|||||||
if (url.pathname === "/v1/chat/completions") {
|
if (url.pathname === "/v1/chat/completions") {
|
||||||
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
||||||
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleChatCompletion(body);
|
return handleChatCompletion(body);
|
||||||
@@ -415,6 +432,8 @@ export default {
|
|||||||
if (url.pathname === "/v1/messages") {
|
if (url.pathname === "/v1/messages") {
|
||||||
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
||||||
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleAnthropicMessages(body);
|
return handleAnthropicMessages(body);
|
||||||
@@ -428,6 +447,8 @@ export default {
|
|||||||
|
|
||||||
// Models list
|
// Models list
|
||||||
if (url.pathname === "/v1/models" && req.method === "GET") {
|
if (url.pathname === "/v1/models" && req.method === "GET") {
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
const models = listModels().map((id) => ({
|
const models = listModels().map((id) => ({
|
||||||
id,
|
id,
|
||||||
object: "model",
|
object: "model",
|
||||||
|
|||||||
@@ -43,6 +43,26 @@ const RELAY_TIMEOUT_MS = Number.parseInt(
|
|||||||
const SERVER_START_TIME = Date.now();
|
const SERVER_START_TIME = Date.now();
|
||||||
const RELAY_VERSION = "1.0.0";
|
const RELAY_VERSION = "1.0.0";
|
||||||
|
|
||||||
|
// ─── API Key Authentication ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const API_KEY = process.env.API_KEY ?? "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a request is authorized.
|
||||||
|
* Returns a 401 Response if unauthorized, or null if allowed.
|
||||||
|
* When API_KEY is empty, all requests pass through.
|
||||||
|
*/
|
||||||
|
function requireAuth(req: Request): Response | null {
|
||||||
|
if (!API_KEY) return null; // auth disabled
|
||||||
|
const header = req.headers.get("authorization") ?? req.headers.get("x-api-key") ?? "";
|
||||||
|
const key = header.replace(/^Bearer\s+/i, "").trim();
|
||||||
|
if (key === API_KEY) return null;
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: { message: "Unauthorized", type: "auth_error" } }),
|
||||||
|
{ status: 401, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Middleware instances (singletons) ───────────────────────────────────────────
|
// ─── Middleware instances (singletons) ───────────────────────────────────────────
|
||||||
|
|
||||||
const rateLimiter = createRateLimiter({
|
const rateLimiter = createRateLimiter({
|
||||||
@@ -483,6 +503,8 @@ const server: Server<WSRelayData> = Bun.serve<WSRelayData>({
|
|||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return new Response("Method Not Allowed", { status: 405 });
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
}
|
}
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleChatCompletion(body, proxyPool);
|
return handleChatCompletion(body, proxyPool);
|
||||||
@@ -502,6 +524,8 @@ const server: Server<WSRelayData> = Bun.serve<WSRelayData>({
|
|||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return new Response("Method Not Allowed", { status: 405 });
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
}
|
}
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleAnthropicMessages(body, proxyPool);
|
return handleAnthropicMessages(body, proxyPool);
|
||||||
@@ -517,6 +541,8 @@ const server: Server<WSRelayData> = Bun.serve<WSRelayData>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === "/v1/models" && req.method === "GET") {
|
if (url.pathname === "/v1/models" && req.method === "GET") {
|
||||||
|
const authErr = requireAuth(req);
|
||||||
|
if (authErr) return authErr;
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
object: "list",
|
object: "list",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export interface Env {
|
|||||||
RATE_LIMIT_WINDOW_MS?: string;
|
RATE_LIMIT_WINDOW_MS?: string;
|
||||||
/** Server listen port (unused on Workers, here for local dev compatibility) */
|
/** Server listen port (unused on Workers, here for local dev compatibility) */
|
||||||
PORT?: string;
|
PORT?: string;
|
||||||
|
/** API key for AI proxy auth (empty = disabled) */
|
||||||
|
API_KEY?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -66,6 +68,19 @@ function getClientIP(req: Request): string {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Auth Helper ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function requireAuth(req: Request, apiKey: string | undefined): Response | null {
|
||||||
|
if (!apiKey) return null; // auth disabled
|
||||||
|
const header = req.headers.get("authorization") ?? req.headers.get("x-api-key") ?? "";
|
||||||
|
const key = header.replace(/^Bearer\s+/i, "").trim();
|
||||||
|
if (key === apiKey) return null;
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: { message: "Unauthorized", type: "auth_error" } }),
|
||||||
|
{ status: 401, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Route Handlers ──────────────────────────────────────────────────────────────
|
// ─── Route Handlers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SERVER_START_TIME = Date.now();
|
const SERVER_START_TIME = Date.now();
|
||||||
@@ -396,6 +411,8 @@ export default {
|
|||||||
if (url.pathname === "/v1/chat/completions") {
|
if (url.pathname === "/v1/chat/completions") {
|
||||||
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
||||||
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
const authErr = requireAuth(req, env.API_KEY);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleChatCompletion(body);
|
return handleChatCompletion(body);
|
||||||
@@ -411,6 +428,8 @@ export default {
|
|||||||
if (url.pathname === "/v1/messages") {
|
if (url.pathname === "/v1/messages") {
|
||||||
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
if (req.method === "OPTIONS") return createCorsPreflightResponse();
|
||||||
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
const authErr = requireAuth(req, env.API_KEY);
|
||||||
|
if (authErr) return authErr;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
return handleAnthropicMessages(body);
|
return handleAnthropicMessages(body);
|
||||||
@@ -424,6 +443,8 @@ export default {
|
|||||||
|
|
||||||
// Models list
|
// Models list
|
||||||
if (url.pathname === "/v1/models" && req.method === "GET") {
|
if (url.pathname === "/v1/models" && req.method === "GET") {
|
||||||
|
const authErr = requireAuth(req, env.API_KEY);
|
||||||
|
if (authErr) return authErr;
|
||||||
const models = listModels().map((id) => ({
|
const models = listModels().map((id) => ({
|
||||||
id,
|
id,
|
||||||
object: "model",
|
object: "model",
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ workers_dev = true
|
|||||||
# RELAY_TIMEOUT_MS = "30000"
|
# RELAY_TIMEOUT_MS = "30000"
|
||||||
# RATE_LIMIT_MAX = "200"
|
# RATE_LIMIT_MAX = "200"
|
||||||
# RATE_LIMIT_WINDOW_MS = "60000"
|
# RATE_LIMIT_WINDOW_MS = "60000"
|
||||||
|
# API_KEY = "sk-your-secret-key"
|
||||||
|
|||||||
Reference in New Issue
Block a user