351 lines
9.6 KiB
TypeScript
351 lines
9.6 KiB
TypeScript
/**
|
|
* DeepLX
|
|
*/
|
|
|
|
import { Hono } from "hono";
|
|
import {
|
|
clearMemoryCache,
|
|
generateCacheKey,
|
|
getCachedTranslation,
|
|
query,
|
|
setCachedTranslation,
|
|
} from "./lib";
|
|
|
|
import { PAYLOAD_LIMITS } from "./lib/config";
|
|
import { createErrorResponse } from "./lib/errorHandler";
|
|
import { normalizeLanguageCode } from "./lib/query";
|
|
import {
|
|
getSecureClientIP,
|
|
handleCORSPreflight,
|
|
validateLanguageCode,
|
|
} from "./lib/security";
|
|
import { translateWithGoogle } from "./lib/services/googleTranslate";
|
|
import { createStandardResponse } from "./lib/types";
|
|
|
|
/**
|
|
* Initialize Hono app with environment bindings
|
|
*/
|
|
const app = new Hono<{ Bindings: Env }>();
|
|
|
|
function isDebugModeEnabled(value?: string): boolean {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Scheduled event handler for periodic maintenance tasks
|
|
* Executes every 5 minutes as configured in wrangler.jsonc
|
|
* @param event The scheduled event object
|
|
* @param env Environment bindings
|
|
* @param ctx Execution context for background tasks
|
|
*/
|
|
function scheduled(
|
|
event: ScheduledEvent,
|
|
env: Env,
|
|
ctx: ExecutionContext
|
|
): void {
|
|
ctx.waitUntil(handleScheduled(event, env));
|
|
}
|
|
|
|
/**
|
|
* Handle scheduled maintenance tasks
|
|
* Performs cache cleanup and other periodic maintenance
|
|
* @param event The scheduled event object
|
|
* @param env Environment bindings
|
|
*/
|
|
async function handleScheduled(event: ScheduledEvent, env: Env): Promise<void> {
|
|
// Clear the in-memory cache every 5 minutes to prevent memory leaks
|
|
clearMemoryCache();
|
|
}
|
|
|
|
/**
|
|
* Worker export configuration
|
|
* Defines the main fetch handler and scheduled event handler
|
|
*/
|
|
const worker = {
|
|
fetch: app.fetch,
|
|
scheduled,
|
|
};
|
|
|
|
export default worker;
|
|
|
|
/**
|
|
* Common translation handler function
|
|
* Processes translation requests for both DeepL and Google Translate
|
|
* @param c - Hono context
|
|
* @param provider - Translation provider ('deepl' or 'google')
|
|
* @returns Translation response
|
|
*/
|
|
async function handleTranslation(c: any, provider: "deepl" | "google") {
|
|
const env = c.env;
|
|
const clientIP = getSecureClientIP(c.req.raw) || "unknown";
|
|
|
|
try {
|
|
// Parse request parameters with better error handling
|
|
let params;
|
|
try {
|
|
params = await c.req.json();
|
|
} catch (parseError) {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
// Enhanced parameter validation with input sanitization
|
|
if (!params || typeof params !== "object") {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
if (!params.text || typeof params.text !== "string") {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
if (!params.text.trim()) {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
// Basic text validation
|
|
let sanitizedText;
|
|
try {
|
|
sanitizedText = params.text;
|
|
if (sanitizedText.length > PAYLOAD_LIMITS.MAX_TEXT_LENGTH) {
|
|
sanitizedText = sanitizedText.slice(0, PAYLOAD_LIMITS.MAX_TEXT_LENGTH);
|
|
}
|
|
} catch (sanitizeError) {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
// Validate text length
|
|
if (!sanitizedText) {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
// Validate and sanitize language parameters
|
|
const sourceLang = params.source_lang
|
|
? validateLanguageCode(params.source_lang)
|
|
: "auto";
|
|
const targetLang = params.target_lang
|
|
? validateLanguageCode(params.target_lang)
|
|
: "en";
|
|
|
|
if (!sourceLang || !targetLang) {
|
|
return c.json(createStandardResponse(400, null), 400);
|
|
}
|
|
|
|
// Check cache first for faster response
|
|
const normalizedSourceLang = normalizeLanguageCode(sourceLang);
|
|
const normalizedTargetLang = normalizeLanguageCode(targetLang);
|
|
const cacheKey = generateCacheKey(
|
|
sanitizedText,
|
|
normalizedSourceLang,
|
|
normalizedTargetLang,
|
|
provider
|
|
);
|
|
const cached = await getCachedTranslation(cacheKey, env);
|
|
|
|
if (cached) {
|
|
return c.json(
|
|
createStandardResponse(
|
|
200,
|
|
cached.data,
|
|
cached.id || Math.floor(Math.random() * 10000000000),
|
|
cached.source_lang,
|
|
cached.target_lang
|
|
)
|
|
);
|
|
}
|
|
|
|
// Prepare validated parameters for translation
|
|
const validatedParams = {
|
|
text: sanitizedText,
|
|
source_lang: normalizedSourceLang,
|
|
target_lang: normalizedTargetLang,
|
|
};
|
|
|
|
let result;
|
|
|
|
// Choose translation provider
|
|
if (provider === "google") {
|
|
result = await translateWithGoogle(validatedParams, {
|
|
env,
|
|
clientIP,
|
|
});
|
|
} else {
|
|
// Use DeepL as default
|
|
result = await query(validatedParams, {
|
|
env,
|
|
clientIP,
|
|
});
|
|
}
|
|
|
|
// Cache successful translations
|
|
if (result.code === 200 && result.data) {
|
|
await setCachedTranslation(
|
|
cacheKey,
|
|
{
|
|
data: result.data,
|
|
timestamp: Date.now(),
|
|
source_lang:
|
|
result.source_lang || validatedParams.source_lang.toUpperCase(),
|
|
target_lang:
|
|
result.target_lang || validatedParams.target_lang.toUpperCase(),
|
|
id: result.id,
|
|
},
|
|
env
|
|
);
|
|
}
|
|
|
|
return c.json(result, result.code as any);
|
|
} catch (error) {
|
|
const errorResponse = createErrorResponse(error, {
|
|
endpoint: `/${provider}`,
|
|
clientIP,
|
|
});
|
|
|
|
return c.json(errorResponse.response, errorResponse.httpStatus as any);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API Route Definitions
|
|
* Defines all available endpoints and their handlers
|
|
*/
|
|
app
|
|
// Add CORS preflight handling for all routes
|
|
.options("*", (c) => handleCORSPreflight(c))
|
|
|
|
.get("/translate", (c) => c.text("Please use POST method :)"))
|
|
.get("/deepl", (c) => c.text("Please use POST method :)"))
|
|
.get("/google", (c) => c.text("Please use POST method :)"))
|
|
|
|
/**
|
|
* Debug endpoint for request format validation and troubleshooting
|
|
* SECURITY: This endpoint is disabled in production unless DEBUG_MODE is explicitly enabled
|
|
* POST /debug
|
|
*/
|
|
.post("/debug", async (c) => {
|
|
// Check if debug mode is enabled via environment variable
|
|
if (!isDebugModeEnabled(c.env.DEBUG_MODE)) {
|
|
return c.json(createStandardResponse(404, null), 404);
|
|
}
|
|
|
|
const env = c.env;
|
|
const clientIP = getSecureClientIP(c.req.raw) || "unknown";
|
|
|
|
try {
|
|
const params = await c.req.json().catch(() => ({}));
|
|
|
|
// Import buildRequestBody from query module for debugging
|
|
const { buildRequestBody } = await import("./lib/query");
|
|
|
|
if (!params.text || typeof params.text !== "string") {
|
|
return c.json(
|
|
createStandardResponse(400, "Missing text parameter"),
|
|
400
|
|
);
|
|
}
|
|
|
|
// Basic text validation
|
|
const sanitizedText = params.text;
|
|
if (!sanitizedText.trim()) {
|
|
return c.json(
|
|
createStandardResponse(400, "Invalid text parameter"),
|
|
400
|
|
);
|
|
}
|
|
|
|
// Validate language codes
|
|
const sourceLang = params.source_lang
|
|
? validateLanguageCode(params.source_lang)
|
|
: "auto";
|
|
const targetLang = params.target_lang
|
|
? validateLanguageCode(params.target_lang)
|
|
: "en";
|
|
|
|
if (!sourceLang || !targetLang) {
|
|
return c.json(
|
|
createStandardResponse(400, "Invalid language codes"),
|
|
400
|
|
);
|
|
}
|
|
|
|
const sanitizedParams = {
|
|
text: sanitizedText,
|
|
source_lang: sourceLang,
|
|
target_lang: targetLang,
|
|
};
|
|
|
|
try {
|
|
const requestBody = buildRequestBody(sanitizedParams);
|
|
const parsedBody = JSON.parse(requestBody);
|
|
|
|
const debugInfo = {
|
|
status: "Request format is valid",
|
|
client_ip: clientIP, // Safe to show in debug mode
|
|
generated_request: parsedBody,
|
|
sanitized_params: sanitizedParams, // Show sanitized version
|
|
validation: {
|
|
text_length: sanitizedText.length,
|
|
sanitized_text_length: sanitizedText.length,
|
|
has_source_lang: !!sourceLang,
|
|
has_target_lang: !!targetLang,
|
|
request_id: parsedBody.id,
|
|
timestamp: parsedBody.params?.timestamp,
|
|
method_format: requestBody.includes('"method" : "')
|
|
? "spaced"
|
|
: "normal",
|
|
normalized_source_lang: sourceLang,
|
|
normalized_target_lang: targetLang,
|
|
},
|
|
};
|
|
|
|
return c.json(
|
|
createStandardResponse(200, JSON.stringify(debugInfo)),
|
|
200
|
|
);
|
|
} catch (buildError) {
|
|
const errorMessage =
|
|
buildError instanceof Error
|
|
? buildError.message
|
|
: "Request build failed";
|
|
return c.json(createStandardResponse(400, errorMessage), 400);
|
|
}
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
return c.json(createStandardResponse(400, errorMessage), 400);
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Main translation endpoint with comprehensive features
|
|
* Handles single text translation with rate limiting, caching, and error handling
|
|
* POST /translate - Uses DeepL (legacy endpoint)
|
|
*/
|
|
.post("/translate", async (c) => {
|
|
return handleTranslation(c, "deepl");
|
|
})
|
|
|
|
/**
|
|
* DeepL translation endpoint
|
|
* POST /deepl - Uses DeepL translation service
|
|
*/
|
|
.post("/deepl", async (c) => {
|
|
return handleTranslation(c, "deepl");
|
|
})
|
|
|
|
/**
|
|
* Google Translate endpoint
|
|
* POST /google - Uses Google Translate service
|
|
*/
|
|
.post("/google", async (c) => {
|
|
return handleTranslation(c, "google");
|
|
})
|
|
|
|
/**
|
|
* Catch-all route for undefined paths
|
|
* Redirects all other requests to the GitHub repository
|
|
*/
|
|
.all("*", (c) => c.redirect("https://github.com/xixu-me/DeepLX"));
|