fix: critical bugs, serverless stability, and security hardening

Bug Fixes:
- Fix rate limiter API mismatch: check() -> checkAsync() in index.ts, worker.ts, api/relay.ts
- Fix WebSocket SSRF silent drop: return error Response instead of undefined
- Fix isDevMode() default: changed from true to false (production-safe)
- Fix process.env -> env bindings in worker.ts requireAuth for Cloudflare Workers
- Fix Bun.file() crash in Workers: add try/catch with fallback
- Fix Bun.CryptoHasher -> Web Crypto API in mimo-auth.ts for Workers compat

Architecture:
- Add public methods to ProxyPool (getEntryAtIndex, getProxyUrlAtIndex, getCurrentIndex, setCurrentIndex) to remove all 'as any' casts in SessionProxyPool
- Add addProxy() method for manual proxy management
- Add loadAsync(), tryLoadAsync(), loadFromString() to ProxyPool

Serverless Stability:
- Add optional DNS rebinding protection via SSRF_DNS_CHECK env flag
- CORS cache now auto-invalidates when CORS_ORIGIN env changes
- Rate limiter max-size eviction (10k keys) prevents unbounded memory growth

Tests:
- Fix type assertions in test files (body as Record<string, unknown>)
- All 153 tests pass, typecheck clean
This commit is contained in:
MythEclipse
2026-06-19 18:10:49 +07:00
parent 6d61dcba8f
commit cb6191902e
13 changed files with 865 additions and 121 deletions
+20 -12
View File
@@ -84,8 +84,8 @@ function getClientIP(req: Request): string {
// --- Auth Helper ---------------------------------------------------------------
function requireAuth(req: Request): Response | null {
const apiKey = process.env.API_KEY ?? "sk-dummy-key";
function requireAuth(req: Request, env: Env): Response | null {
const apiKey = env.API_KEY ?? "sk-dummy-key";
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;
@@ -268,7 +268,7 @@ async function handleRelay(req: Request, env: Env): Promise<Response> {
}
// -- Middleware: Rate limiting ---------------------------------------------
const rateCheck = limiter.check(clientIP);
const rateCheck = await limiter.checkAsync(clientIP);
if (!rateCheck.allowed) {
logRelayEvent({
method,
@@ -387,12 +387,20 @@ export default {
if (url.pathname === "/health") return handleHealth();
if (url.pathname === "/docs" || url.pathname === "/test") {
const file = Bun.file("public/test-api.html");
const exists = await file.exists();
return new Response(exists ? file : "Not found", {
status: exists ? 200 : 404,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
try {
const file = Bun.file("public/test-api.html");
const exists = await file.exists();
return new Response(exists ? file : "Not found", {
status: exists ? 200 : 404,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
} catch {
// Bun.file not available in non-Bun runtimes (e.g. Workers)
return new Response("Not found", {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
}
if (
url.pathname === "/" &&
@@ -425,7 +433,7 @@ export default {
if (url.pathname === "/v1/chat/completions") {
if (req.method === "OPTIONS") return createCorsPreflightResponse();
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const authErr = requireAuth(req);
const authErr = requireAuth(req, env);
if (authErr) return authErr;
try {
const body = await req.json();
@@ -441,7 +449,7 @@ export default {
if (url.pathname === "/v1/messages") {
if (req.method === "OPTIONS") return createCorsPreflightResponse();
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const authErr = requireAuth(req);
const authErr = requireAuth(req, env);
if (authErr) return authErr;
try {
const body = await req.json();
@@ -455,7 +463,7 @@ export default {
}
if (url.pathname === "/v1/models" && req.method === "GET") {
const authErr = requireAuth(req);
const authErr = requireAuth(req, env);
if (authErr) return authErr;
const models = listModels().map((id) => ({
id,