Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function verifyWalletSignature(
}

// Type guard for request.body
const { walletAddress, message, signature } = request.body as Record<string, unknown>;
const { walletAddress, message, signature } = request.body as { walletAddress: string; message: string; signature: string };
if (
typeof walletAddress !== 'string' ||
typeof message !== 'string' ||
Expand All @@ -50,6 +50,5 @@ export async function verifyWalletSignature(
error ? { reason: error } : undefined
);
}

// If all validation passes, continue
}
35 changes: 19 additions & 16 deletions src/middleware/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@

import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

/**
/**
* Rate limiting configuration for different endpoint types
*/
export const rateLimitConfigs = {
// General API rate limit (most endpoints)
// General API rate limit
general: {
max: parseInt(process.env.RATE_LIMIT_GENERAL_MAX || '100'), // 100 requests
timeWindow: 60 * 1000, // per minute
skipSuccessfulRequests: false,
skipOnError: false,
},

// Authentication endpoints (stricter limits)
// Authentication endpoints
auth: {
max: parseInt(process.env.RATE_LIMIT_AUTH_MAX || '10'), // 10 requests
timeWindow: 60 * 1000, // per minute
skipSuccessfulRequests: false,
skipOnError: false,
},

// API key creation (very strict)
// API key creation
apiKeyCreation: {
max: parseInt(process.env.RATE_LIMIT_API_KEY_CREATION_MAX || '3'), // 3 API key creations
timeWindow: 60 * 1000, // per minute
skipSuccessfulRequests: false,
skipOnError: false,
},

// Registration (moderate limits)
// Registration
registration: {
max: parseInt(process.env.RATE_LIMIT_REGISTRATION_MAX || '5'), // 5 registration attempts
timeWindow: 60 * 1000, // per minute
skipSuccessfulRequests: false,
skipOnError: false,
},

// Health checks (more lenient)
// Health checks
health: {
max: parseInt(process.env.RATE_LIMIT_HEALTH_MAX || '200'), // 200 requests
timeWindow: 60 * 1000, // per minute
Expand All @@ -45,6 +47,7 @@ export const rateLimitConfigs = {
}
};

/**
/**
* Custom error response for rate limit exceeded
*/
Expand All @@ -53,61 +56,61 @@ export const rateLimitErrorHandler = (request: FastifyRequest, context: any) =>
statusCode: 429,
error: 'Rate limit exceeded',
message: `Too many requests. Try again in ${Math.ceil(context.ttl / 1000)} seconds.`,
retryAfter: Math.ceil(context.ttl / 1000)
retryAfter: Math.ceil(context.ttl / 1000),
};
};

/**
/**
* Key generator for rate limiting - uses IP + wallet address if available
*/
export const rateLimitKeyGenerator = (request: FastifyRequest) => {
const ip = request.ip;
let walletAddress: string | undefined;
if (request.body && typeof request.body === 'object' && 'walletAddress' in request.body) {
walletAddress = (request.body as any).walletAddress;
walletAddress = (request.body as { walletAddress?: string }).walletAddress;
}
if (!walletAddress && request.headers['x-wallet-address']) {
walletAddress = String(request.headers['x-wallet-address']);
}
// If wallet address is available, use IP + wallet for more granular control
if (walletAddress) {
return `${ip}:${walletAddress.toLowerCase()}`;
}
// Fallback to just IP
return ip;
Comment on lines 70 to 79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard walletAddress before calling toLowerCase to prevent runtime crashes

walletAddress from the body can be non‑string (e.g., number/array), which would throw on .toLowerCase(). Add a type check (and optionally trim) before usage. Also gate the final branch on a string.

   let walletAddress: string | undefined;
   if (request.body && typeof request.body === 'object' && 'walletAddress' in request.body) {
-    walletAddress = (request.body as { walletAddress?: string }).walletAddress;
+    const raw = (request.body as { walletAddress?: unknown }).walletAddress;
+    if (typeof raw === 'string' && raw.trim() !== '') {
+      walletAddress = raw;
+    }
   }
   if (!walletAddress && request.headers['x-wallet-address']) {
     walletAddress = String(request.headers['x-wallet-address']);
   }
-  if (walletAddress) {
+  if (typeof walletAddress === 'string' && walletAddress) {
     return `${ip}:${walletAddress.toLowerCase()}`;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (request.body && typeof request.body === 'object' && 'walletAddress' in request.body) {
walletAddress = (request.body as any).walletAddress;
walletAddress = (request.body as { walletAddress?: string }).walletAddress;
}
if (!walletAddress && request.headers['x-wallet-address']) {
walletAddress = String(request.headers['x-wallet-address']);
}
// If wallet address is available, use IP + wallet for more granular control
if (walletAddress) {
return `${ip}:${walletAddress.toLowerCase()}`;
}
// Fallback to just IP
return ip;
if (request.body && typeof request.body === 'object' && 'walletAddress' in request.body) {
const raw = (request.body as { walletAddress?: unknown }).walletAddress;
if (typeof raw === 'string' && raw.trim() !== '') {
walletAddress = raw;
}
}
if (!walletAddress && request.headers['x-wallet-address']) {
walletAddress = String(request.headers['x-wallet-address']);
}
if (typeof walletAddress === 'string' && walletAddress) {
return `${ip}:${walletAddress.toLowerCase()}`;
}
return ip;
🤖 Prompt for AI Agents
In src/middleware/rateLimit.ts around lines 70 to 79, the code calls
walletAddress.toLowerCase() without ensuring walletAddress is a string; if the
body/header contains a non-string (number, array, object) this will throw. Fix
by checking typeof walletAddress === 'string' (and optionally trimming) before
calling toLowerCase; when reading from request.headers coerce to string only
after verifying it's a string-like value or use String(...) and then guard that
the resulting value is a non-empty string before lowercasing; if walletAddress
is not a valid string, fall back to returning ip.

};

/**
/**
* Register rate limiting plugin with different configurations
*/
export async function registerRateLimiting(fastify: FastifyInstance) {
// Register the rate limit plugin
await fastify.register(import('@fastify/rate-limit'), {
global: false, // We'll apply different limits per route
global: false,
errorResponseBuilder: rateLimitErrorHandler,
keyGenerator: rateLimitKeyGenerator,
addHeadersOnExceeding: {
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true
'x-ratelimit-reset': true,
},
addHeaders: {
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true
}
'x-ratelimit-reset': true,
},
});
}

/**
/**
* Create rate limit preHandler for specific configurations
*/
export function createRateLimitHandler(config: typeof rateLimitConfigs.general) {
return {
config,
preHandler: async (request: FastifyRequest, reply: FastifyReply) => {
// You can add custom logic here if needed
// Custom logic can be added here if needed
return;
}
},
};
}