feat: migrate to Hono framework
- Add Hono as HTTP framework for Cloudflare Workers - Create app.ts with declarative routing and middleware - Add hono-adapters.ts for auth, rate limit, request ID middleware - Refactor handlers to use Hono Context signature - Maintain all existing business logic unchanged Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
191
src/app.ts
Normal file
191
src/app.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Hono Application Setup
|
||||
*
|
||||
* Configures Hono app with CORS, security headers, and routes.
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import type { Env } from './types';
|
||||
import { CORS, HTTP_STATUS } from './constants';
|
||||
import { createLogger } from './utils/logger';
|
||||
import {
|
||||
requestIdMiddleware,
|
||||
authMiddleware,
|
||||
rateLimitMiddleware,
|
||||
optionalAuthMiddleware,
|
||||
} from './middleware/hono-adapters';
|
||||
import { handleHealth } from './routes/health';
|
||||
import { handleInstances } from './routes/instances';
|
||||
import { handleSync } from './routes/sync';
|
||||
|
||||
const logger = createLogger('[App]');
|
||||
|
||||
// Context variables type
|
||||
type Variables = {
|
||||
requestId: string;
|
||||
authenticated?: boolean;
|
||||
};
|
||||
|
||||
// Create Hono app with type-safe bindings
|
||||
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||
|
||||
/**
|
||||
* Get CORS origin for request
|
||||
* Reused from original index.ts logic
|
||||
*/
|
||||
function getCorsOrigin(c: Context<{ Bindings: Env; Variables: Variables }>): string {
|
||||
const origin = c.req.header('Origin');
|
||||
const env = c.env;
|
||||
|
||||
// Environment variable has explicit origin configured (highest priority)
|
||||
if (env.CORS_ORIGIN && env.CORS_ORIGIN !== '*') {
|
||||
return env.CORS_ORIGIN;
|
||||
}
|
||||
|
||||
// Build allowed origins list based on environment
|
||||
const isDevelopment = env.ENVIRONMENT === 'development';
|
||||
const allowedOrigins = isDevelopment
|
||||
? [...CORS.ALLOWED_ORIGINS, ...CORS.DEVELOPMENT_ORIGINS]
|
||||
: CORS.ALLOWED_ORIGINS;
|
||||
|
||||
// Request origin is in allowed list
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Log unmatched origins for security monitoring
|
||||
if (origin && !allowedOrigins.includes(origin)) {
|
||||
const sanitizedOrigin = origin.replace(/[\r\n\t]/g, '').substring(0, 256);
|
||||
logger.warn('Unmatched origin - using default', {
|
||||
requested_origin: sanitizedOrigin,
|
||||
environment: env.ENVIRONMENT || 'production',
|
||||
default_origin: CORS.DEFAULT_ORIGIN,
|
||||
});
|
||||
}
|
||||
|
||||
// Return explicit default (no wildcard)
|
||||
return CORS.DEFAULT_ORIGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS middleware
|
||||
* Configured dynamically based on request origin
|
||||
*/
|
||||
app.use('*', async (c, next) => {
|
||||
// Handle OPTIONS preflight - must come before await next()
|
||||
if (c.req.method === 'OPTIONS') {
|
||||
const origin = getCorsOrigin(c);
|
||||
c.res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
||||
c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
||||
return c.body(null, 204);
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
// Set CORS headers after processing
|
||||
const origin = getCorsOrigin(c);
|
||||
c.res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
||||
c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
||||
c.res.headers.set(
|
||||
'Access-Control-Expose-Headers',
|
||||
'X-RateLimit-Retry-After, Retry-After, X-Request-ID'
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Request ID middleware
|
||||
* Adds unique request ID for tracing
|
||||
*/
|
||||
app.use('*', requestIdMiddleware);
|
||||
|
||||
/**
|
||||
* Security headers middleware
|
||||
* Applied to all responses
|
||||
*/
|
||||
app.use('*', async (c, next) => {
|
||||
await next();
|
||||
|
||||
// Add security headers to response
|
||||
c.res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
c.res.headers.set('X-Frame-Options', 'DENY');
|
||||
c.res.headers.set('Strict-Transport-Security', 'max-age=31536000');
|
||||
c.res.headers.set('Content-Security-Policy', "default-src 'none'");
|
||||
c.res.headers.set('X-XSS-Protection', '1; mode=block');
|
||||
c.res.headers.set('Referrer-Policy', 'no-referrer');
|
||||
});
|
||||
|
||||
/**
|
||||
* Environment validation middleware
|
||||
* Checks required environment variables before processing
|
||||
*/
|
||||
app.use('*', async (c, next) => {
|
||||
const required = ['API_KEY'];
|
||||
const missing = required.filter((key) => !c.env[key as keyof Env]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
logger.error('Missing required environment variables', {
|
||||
missing,
|
||||
request_id: c.get('requestId'),
|
||||
});
|
||||
|
||||
return c.json(
|
||||
{
|
||||
error: 'Service Unavailable',
|
||||
message: 'Service configuration error',
|
||||
},
|
||||
503
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Routes
|
||||
*/
|
||||
|
||||
// Health check (public endpoint with optional authentication)
|
||||
app.get('/health', optionalAuthMiddleware, handleHealth);
|
||||
|
||||
// Query instances (authenticated, rate limited)
|
||||
app.get('/instances', authMiddleware, rateLimitMiddleware, handleInstances);
|
||||
|
||||
// Sync trigger (authenticated, rate limited)
|
||||
app.post('/sync', authMiddleware, rateLimitMiddleware, handleSync);
|
||||
|
||||
/**
|
||||
* 404 handler
|
||||
*/
|
||||
app.notFound((c) => {
|
||||
return c.json(
|
||||
{
|
||||
error: 'Not Found',
|
||||
path: c.req.path,
|
||||
},
|
||||
HTTP_STATUS.NOT_FOUND
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Global error handler
|
||||
*/
|
||||
app.onError((err, c) => {
|
||||
logger.error('Request error', {
|
||||
error: err,
|
||||
request_id: c.get('requestId'),
|
||||
});
|
||||
|
||||
return c.json(
|
||||
{
|
||||
error: 'Internal Server Error',
|
||||
},
|
||||
HTTP_STATUS.INTERNAL_ERROR
|
||||
);
|
||||
});
|
||||
|
||||
export default app;
|
||||
190
src/index.ts
190
src/index.ts
@@ -5,199 +5,15 @@
|
||||
*/
|
||||
|
||||
import { Env } from './types';
|
||||
import { handleSync, handleInstances, handleHealth } from './routes';
|
||||
import {
|
||||
authenticateRequest,
|
||||
verifyApiKey,
|
||||
createUnauthorizedResponse,
|
||||
checkRateLimit,
|
||||
createRateLimitResponse,
|
||||
} from './middleware';
|
||||
import { CORS, HTTP_STATUS } from './constants';
|
||||
import app from './app';
|
||||
import { createLogger } from './utils/logger';
|
||||
import { SyncOrchestrator } from './services/sync';
|
||||
|
||||
/**
|
||||
* Validate required environment variables
|
||||
*/
|
||||
function validateEnv(env: Env): { valid: boolean; missing: string[] } {
|
||||
const required = ['API_KEY'];
|
||||
const missing = required.filter(key => !env[key as keyof Env]);
|
||||
return { valid: missing.length === 0, missing };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CORS origin for request
|
||||
*
|
||||
* Security: No wildcard fallback. Returns explicit allowed origin or default.
|
||||
* Logs unmatched origins for monitoring.
|
||||
*/
|
||||
function getCorsOrigin(request: Request, env: Env): string {
|
||||
const origin = request.headers.get('Origin');
|
||||
const logger = createLogger('[CORS]', env);
|
||||
|
||||
// Environment variable has explicit origin configured (highest priority)
|
||||
if (env.CORS_ORIGIN && env.CORS_ORIGIN !== '*') {
|
||||
return env.CORS_ORIGIN;
|
||||
}
|
||||
|
||||
// Build allowed origins list based on environment
|
||||
const isDevelopment = env.ENVIRONMENT === 'development';
|
||||
const allowedOrigins = isDevelopment
|
||||
? [...CORS.ALLOWED_ORIGINS, ...CORS.DEVELOPMENT_ORIGINS]
|
||||
: CORS.ALLOWED_ORIGINS;
|
||||
|
||||
// Request origin is in allowed list
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Log unmatched origins for security monitoring
|
||||
if (origin && !allowedOrigins.includes(origin)) {
|
||||
// Sanitize origin to prevent log injection (remove control characters)
|
||||
const sanitizedOrigin = origin.replace(/[\r\n\t]/g, '').substring(0, 256);
|
||||
logger.warn('Unmatched origin - using default', {
|
||||
requested_origin: sanitizedOrigin,
|
||||
environment: env.ENVIRONMENT || 'production',
|
||||
default_origin: CORS.DEFAULT_ORIGIN
|
||||
});
|
||||
}
|
||||
|
||||
// Return explicit default (no wildcard)
|
||||
return CORS.DEFAULT_ORIGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add security headers to response
|
||||
* Performance optimization: Reuses response body without cloning to minimize memory allocation
|
||||
*
|
||||
* Benefits:
|
||||
* - Avoids Response.clone() which copies the entire body stream
|
||||
* - Directly references response.body (ReadableStream) without duplication
|
||||
* - Reduces memory allocation and GC pressure per request
|
||||
*
|
||||
* Note: response.body can be null for 204 No Content or empty responses
|
||||
*/
|
||||
function addSecurityHeaders(response: Response, corsOrigin?: string, requestId?: string): Response {
|
||||
const headers = new Headers(response.headers);
|
||||
|
||||
// Basic security headers
|
||||
headers.set('X-Content-Type-Options', 'nosniff');
|
||||
headers.set('X-Frame-Options', 'DENY');
|
||||
headers.set('Strict-Transport-Security', 'max-age=31536000');
|
||||
|
||||
// CORS headers
|
||||
headers.set('Access-Control-Allow-Origin', corsOrigin || CORS.DEFAULT_ORIGIN);
|
||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
||||
headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
||||
headers.set('Access-Control-Expose-Headers', 'X-RateLimit-Retry-After, Retry-After, X-Request-ID');
|
||||
|
||||
// Additional security headers
|
||||
headers.set('Content-Security-Policy', "default-src 'none'");
|
||||
headers.set('X-XSS-Protection', '1; mode=block');
|
||||
headers.set('Referrer-Policy', 'no-referrer');
|
||||
|
||||
// Request ID for audit trail
|
||||
if (requestId) {
|
||||
headers.set('X-Request-ID', requestId);
|
||||
}
|
||||
|
||||
// Create new Response with same body reference (no copy) and updated headers
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* HTTP Request Handler
|
||||
* HTTP Request Handler (delegated to Hono)
|
||||
*/
|
||||
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// Generate request ID for audit trail (use CF-Ray if available, otherwise generate UUID)
|
||||
const requestId = request.headers.get('CF-Ray') || crypto.randomUUID();
|
||||
|
||||
// Get CORS origin based on request and configuration
|
||||
const corsOrigin = getCorsOrigin(request, env);
|
||||
|
||||
try {
|
||||
// Handle OPTIONS preflight requests
|
||||
if (request.method === 'OPTIONS') {
|
||||
return addSecurityHeaders(new Response(null, { status: 204 }), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// Validate required environment variables
|
||||
const envValidation = validateEnv(env);
|
||||
if (!envValidation.valid) {
|
||||
const logger = createLogger('[Worker]');
|
||||
logger.error('Missing required environment variables', { missing: envValidation.missing, request_id: requestId });
|
||||
return addSecurityHeaders(
|
||||
Response.json(
|
||||
{ error: 'Service Unavailable', message: 'Service configuration error' },
|
||||
{ status: 503 }
|
||||
),
|
||||
corsOrigin,
|
||||
requestId
|
||||
);
|
||||
}
|
||||
|
||||
// Health check (public endpoint with optional authentication)
|
||||
if (path === '/health') {
|
||||
const apiKey = request.headers.get('X-API-Key');
|
||||
const authenticated = apiKey ? verifyApiKey(apiKey, env) : false;
|
||||
return addSecurityHeaders(await handleHealth(env, authenticated), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// Authentication required for all other endpoints
|
||||
const isAuthenticated = await authenticateRequest(request, env);
|
||||
if (!isAuthenticated) {
|
||||
return addSecurityHeaders(createUnauthorizedResponse(), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// Rate limiting for authenticated endpoints
|
||||
const rateLimitCheck = await checkRateLimit(request, path, env);
|
||||
if (!rateLimitCheck.allowed) {
|
||||
return addSecurityHeaders(createRateLimitResponse(rateLimitCheck.retryAfter!), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// Query instances
|
||||
if (path === '/instances' && request.method === 'GET') {
|
||||
return addSecurityHeaders(await handleInstances(request, env), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// Sync trigger
|
||||
if (path === '/sync' && request.method === 'POST') {
|
||||
return addSecurityHeaders(await handleSync(request, env), corsOrigin, requestId);
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
return addSecurityHeaders(
|
||||
Response.json(
|
||||
{ error: 'Not Found', path },
|
||||
{ status: HTTP_STATUS.NOT_FOUND }
|
||||
),
|
||||
corsOrigin,
|
||||
requestId
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
const logger = createLogger('[Worker]');
|
||||
logger.error('Request error', { error, request_id: requestId });
|
||||
return addSecurityHeaders(
|
||||
Response.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: HTTP_STATUS.INTERNAL_ERROR }
|
||||
),
|
||||
corsOrigin,
|
||||
requestId
|
||||
);
|
||||
}
|
||||
},
|
||||
fetch: app.fetch,
|
||||
|
||||
/**
|
||||
* Scheduled (Cron) Handler
|
||||
|
||||
110
src/middleware/hono-adapters.ts
Normal file
110
src/middleware/hono-adapters.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Hono Middleware Adapters
|
||||
*
|
||||
* Adapts existing authentication and rate limiting middleware to Hono's middleware pattern.
|
||||
*/
|
||||
|
||||
import type { Context, Next } from 'hono';
|
||||
import type { Env } from '../types';
|
||||
import {
|
||||
authenticateRequest,
|
||||
verifyApiKey,
|
||||
createUnauthorizedResponse,
|
||||
} from './auth';
|
||||
import { checkRateLimit, createRateLimitResponse } from './rateLimit';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
const logger = createLogger('[Middleware]');
|
||||
|
||||
// Context variables type
|
||||
type Variables = {
|
||||
requestId: string;
|
||||
authenticated?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request ID middleware
|
||||
* Adds unique request ID to context for tracing
|
||||
*/
|
||||
export async function requestIdMiddleware(
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||
next: Next
|
||||
): Promise<void> {
|
||||
// Use CF-Ray if available, otherwise generate UUID
|
||||
const requestId = c.req.header('CF-Ray') || crypto.randomUUID();
|
||||
|
||||
// Store in context for handlers to use
|
||||
c.set('requestId', requestId);
|
||||
|
||||
await next();
|
||||
|
||||
// Add to response headers
|
||||
c.res.headers.set('X-Request-ID', requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Validates X-API-Key header using existing auth logic
|
||||
*/
|
||||
export async function authMiddleware(
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||
next: Next
|
||||
): Promise<Response | void> {
|
||||
const request = c.req.raw;
|
||||
const env = c.env;
|
||||
|
||||
const isAuthenticated = await authenticateRequest(request, env);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
logger.warn('[Auth] Unauthorized request', {
|
||||
path: c.req.path,
|
||||
requestId: c.get('requestId'),
|
||||
});
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting middleware
|
||||
* Applies rate limits based on endpoint using existing rate limit logic
|
||||
*/
|
||||
export async function rateLimitMiddleware(
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||
next: Next
|
||||
): Promise<Response | void> {
|
||||
const request = c.req.raw;
|
||||
const path = c.req.path;
|
||||
const env = c.env;
|
||||
|
||||
const rateLimitCheck = await checkRateLimit(request, path, env);
|
||||
|
||||
if (!rateLimitCheck.allowed) {
|
||||
logger.warn('[RateLimit] Rate limit exceeded', {
|
||||
path,
|
||||
retryAfter: rateLimitCheck.retryAfter,
|
||||
requestId: c.get('requestId'),
|
||||
});
|
||||
return createRateLimitResponse(rateLimitCheck.retryAfter!);
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional authentication middleware for health check
|
||||
* Checks if API key is provided and valid, stores result in context
|
||||
*/
|
||||
export async function optionalAuthMiddleware(
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||
next: Next
|
||||
): Promise<void> {
|
||||
const apiKey = c.req.header('X-API-Key');
|
||||
const authenticated = apiKey ? verifyApiKey(apiKey, c.env) : false;
|
||||
|
||||
// Store authentication status in context
|
||||
c.set('authenticated', authenticated);
|
||||
|
||||
await next();
|
||||
}
|
||||
@@ -3,12 +3,19 @@
|
||||
* Comprehensive health monitoring for database and provider sync status
|
||||
*/
|
||||
|
||||
import type { Context } from 'hono';
|
||||
import { Env } from '../types';
|
||||
import { HTTP_STATUS } from '../constants';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
const logger = createLogger('[Health]');
|
||||
|
||||
// Context variables type
|
||||
type Variables = {
|
||||
requestId: string;
|
||||
authenticated?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component health status
|
||||
*/
|
||||
@@ -159,18 +166,17 @@ function sanitizeError(error: string): string {
|
||||
|
||||
/**
|
||||
* Handle health check request
|
||||
* @param env - Cloudflare Worker environment
|
||||
* @param authenticated - Whether the request is authenticated (default: false)
|
||||
* @param c - Hono context
|
||||
*/
|
||||
export async function handleHealth(
|
||||
env: Env,
|
||||
authenticated: boolean = false
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const authenticated = c.get('authenticated') ?? false;
|
||||
|
||||
try {
|
||||
// Check database health
|
||||
const dbHealth = await checkDatabaseHealth(env.DB);
|
||||
const dbHealth = await checkDatabaseHealth(c.env.DB);
|
||||
|
||||
// If database is unhealthy, return early
|
||||
if (dbHealth.status === 'unhealthy') {
|
||||
@@ -206,7 +212,7 @@ export async function handleHealth(
|
||||
}
|
||||
|
||||
// Get all providers with aggregated counts in a single query
|
||||
const providersWithCounts = await env.DB.prepare(`
|
||||
const providersWithCounts = await c.env.DB.prepare(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
|
||||
@@ -5,10 +5,17 @@
|
||||
* Integrates with cache service for performance optimization.
|
||||
*/
|
||||
|
||||
import type { Context } from 'hono';
|
||||
import type { Env, InstanceQueryParams } from '../types';
|
||||
import { QueryService } from '../services/query';
|
||||
import { getGlobalCacheService } from '../services/cache';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Context variables type
|
||||
type Variables = {
|
||||
requestId: string;
|
||||
authenticated?: boolean;
|
||||
};
|
||||
import {
|
||||
SUPPORTED_PROVIDERS,
|
||||
type SupportedProvider,
|
||||
@@ -311,24 +318,22 @@ function parseQueryParams(url: URL): {
|
||||
/**
|
||||
* Handle GET /instances endpoint
|
||||
*
|
||||
* @param request - HTTP request object
|
||||
* @param env - Cloudflare Worker environment bindings
|
||||
* @param c - Hono context
|
||||
* @returns JSON response with instance query results
|
||||
*
|
||||
* @example
|
||||
* GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50
|
||||
*/
|
||||
export async function handleInstances(
|
||||
request: Request,
|
||||
env: Env
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.info('[Instances] Request received', { url: request.url });
|
||||
logger.info('[Instances] Request received', { url: c.req.url });
|
||||
|
||||
try {
|
||||
// Parse URL and query parameters
|
||||
const url = new URL(request.url);
|
||||
const url = new URL(c.req.url);
|
||||
const parseResult = parseQueryParams(url);
|
||||
|
||||
// Handle validation errors
|
||||
@@ -347,7 +352,7 @@ export async function handleInstances(
|
||||
logger.info('[Instances] Query params validated', params as unknown as Record<string, unknown>);
|
||||
|
||||
// Get global cache service singleton (shared across all routes)
|
||||
const cacheService = getGlobalCacheService(CACHE_TTL.INSTANCES, env.RATE_LIMIT_KV);
|
||||
const cacheService = getGlobalCacheService(CACHE_TTL.INSTANCES, c.env.RATE_LIMIT_KV);
|
||||
|
||||
// Generate cache key from query parameters
|
||||
const cacheKey = cacheService.generateKey(params as unknown as Record<string, unknown>);
|
||||
@@ -421,7 +426,7 @@ export async function handleInstances(
|
||||
};
|
||||
|
||||
// Get QueryService singleton (reused across requests)
|
||||
const queryService = getQueryService(env.DB, env);
|
||||
const queryService = getQueryService(c.env.DB, c.env);
|
||||
const result = await queryService.queryInstances(queryParams);
|
||||
|
||||
const queryTime = Date.now() - startTime;
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
* Validates request parameters and orchestrates sync operations.
|
||||
*/
|
||||
|
||||
import type { Context } from 'hono';
|
||||
import type { Env } from '../types';
|
||||
import { SyncOrchestrator } from '../services/sync';
|
||||
import { logger } from '../utils/logger';
|
||||
import { SUPPORTED_PROVIDERS, HTTP_STATUS } from '../constants';
|
||||
import { parseJsonBody, validateProviders, createErrorResponse } from '../utils/validation';
|
||||
|
||||
// Context variables type
|
||||
type Variables = {
|
||||
requestId: string;
|
||||
authenticated?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request body interface for sync endpoint
|
||||
*/
|
||||
@@ -23,8 +30,7 @@ interface SyncRequestBody {
|
||||
/**
|
||||
* Handle POST /sync endpoint
|
||||
*
|
||||
* @param request - HTTP request object
|
||||
* @param env - Cloudflare Worker environment bindings
|
||||
* @param c - Hono context
|
||||
* @returns JSON response with sync results
|
||||
*
|
||||
* @example
|
||||
@@ -35,8 +41,7 @@ interface SyncRequestBody {
|
||||
* }
|
||||
*/
|
||||
export async function handleSync(
|
||||
request: Request,
|
||||
env: Env
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
const startTime = Date.now();
|
||||
const startedAt = new Date().toISOString();
|
||||
@@ -45,7 +50,7 @@ export async function handleSync(
|
||||
|
||||
try {
|
||||
// Validate content-length before parsing body
|
||||
const contentLength = request.headers.get('content-length');
|
||||
const contentLength = c.req.header('content-length');
|
||||
if (contentLength) {
|
||||
const bodySize = parseInt(contentLength, 10);
|
||||
if (isNaN(bodySize) || bodySize > 10240) { // 10KB limit for sync
|
||||
@@ -57,12 +62,12 @@ export async function handleSync(
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const contentType = request.headers.get('content-type');
|
||||
const contentType = c.req.header('content-type');
|
||||
let body: SyncRequestBody = {};
|
||||
|
||||
// Only parse JSON if content-type is set
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const parseResult = await parseJsonBody<SyncRequestBody>(request);
|
||||
const parseResult = await parseJsonBody<SyncRequestBody>(c.req.raw);
|
||||
if (!parseResult.success) {
|
||||
logger.error('[Sync] Invalid JSON in request body', {
|
||||
code: parseResult.error.code,
|
||||
@@ -90,7 +95,7 @@ export async function handleSync(
|
||||
logger.info('[Sync] Validation passed', { providers, force });
|
||||
|
||||
// Initialize SyncOrchestrator
|
||||
const orchestrator = new SyncOrchestrator(env.DB, env);
|
||||
const orchestrator = new SyncOrchestrator(c.env.DB, c.env);
|
||||
|
||||
// Execute synchronization
|
||||
logger.info('[Sync] Starting synchronization', { providers });
|
||||
|
||||
Reference in New Issue
Block a user