feat: comprehensive project structure improvements and Cloud for Sovereignty landing zone
- Add Cloud for Sovereignty landing zone architecture and deployment - Implement complete legal document management system - Reorganize documentation with improved navigation - Add infrastructure improvements (Dockerfiles, K8s, monitoring) - Add operational improvements (graceful shutdown, rate limiting, caching) - Create comprehensive project structure documentation - Add Azure deployment automation scripts - Improve repository navigation and organization
This commit is contained in:
105
packages/shared/src/graceful-shutdown.ts
Normal file
105
packages/shared/src/graceful-shutdown.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Graceful shutdown utilities
|
||||
* Handles signal processing, connection draining, and in-flight request completion
|
||||
*/
|
||||
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logger = createLogger('graceful-shutdown');
|
||||
|
||||
export interface GracefulShutdownOptions {
|
||||
timeout?: number; // Maximum time to wait for shutdown (default: 30s)
|
||||
onShutdown?: () => Promise<void>; // Custom shutdown hook
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup graceful shutdown for a Fastify server
|
||||
*/
|
||||
export function setupGracefulShutdown(
|
||||
server: { close: () => Promise<void> },
|
||||
options: GracefulShutdownOptions = {}
|
||||
): void {
|
||||
const { timeout = 30000, onShutdown } = options;
|
||||
|
||||
const shutdown = async (signal: string): Promise<void> => {
|
||||
logger.info({ signal }, 'Received shutdown signal, starting graceful shutdown...');
|
||||
|
||||
// Stop accepting new connections
|
||||
server.close = (() => {
|
||||
const originalClose = server.close.bind(server);
|
||||
return async () => {
|
||||
logger.info('Server closed, no longer accepting new connections');
|
||||
return originalClose();
|
||||
};
|
||||
})();
|
||||
|
||||
try {
|
||||
// Wait for in-flight requests with timeout
|
||||
const shutdownPromise = Promise.all([
|
||||
// Wait for server to close
|
||||
server.close().catch((err) => {
|
||||
logger.warn({ err }, 'Error closing server');
|
||||
}),
|
||||
// Run custom shutdown hook if provided
|
||||
onShutdown?.().catch((err) => {
|
||||
logger.warn({ err }, 'Error in shutdown hook');
|
||||
}),
|
||||
]);
|
||||
|
||||
// Apply timeout
|
||||
await Promise.race([
|
||||
shutdownPromise,
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
logger.warn('Shutdown timeout reached, forcing exit');
|
||||
resolve();
|
||||
}, timeout);
|
||||
}),
|
||||
]);
|
||||
|
||||
logger.info('Graceful shutdown complete');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Error during graceful shutdown');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle termination signals
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error({ err }, 'Uncaught exception, shutting down');
|
||||
shutdown('uncaughtException').catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logger.error({ reason }, 'Unhandled rejection, shutting down');
|
||||
shutdown('unhandledRejection').catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connection drainer for database connections
|
||||
*/
|
||||
export function createConnectionDrainer(
|
||||
pool: { end: () => Promise<void> }
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
logger.info('Draining database connections...');
|
||||
try {
|
||||
await pool.end();
|
||||
logger.info('Database connections drained');
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Error draining database connections');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export * from './middleware';
|
||||
export * from './validation';
|
||||
export * from './auth';
|
||||
export * from './rate-limit-credential';
|
||||
export * from './rate-limiting';
|
||||
export * from './graceful-shutdown';
|
||||
export * from './rate-limit-entra';
|
||||
export * from './authorization';
|
||||
export * from './compliance';
|
||||
|
||||
173
packages/shared/src/rate-limiting.ts
Normal file
173
packages/shared/src/rate-limiting.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Global rate limiting implementation
|
||||
* Supports per-user, per-IP, and global rate limits
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logger = createLogger('rate-limiting');
|
||||
|
||||
export interface RateLimitOptions {
|
||||
maxRequests?: number;
|
||||
windowMs?: number;
|
||||
keyGenerator?: (request: FastifyRequest) => string;
|
||||
skipOnError?: boolean;
|
||||
}
|
||||
|
||||
interface RateLimitStore {
|
||||
[key: string]: {
|
||||
count: number;
|
||||
resetTime: number;
|
||||
};
|
||||
}
|
||||
|
||||
const store: RateLimitStore = {};
|
||||
|
||||
/**
|
||||
* Create a rate limit key from request
|
||||
*/
|
||||
function generateKey(request: FastifyRequest, options: RateLimitOptions): string {
|
||||
if (options.keyGenerator) {
|
||||
return options.keyGenerator(request);
|
||||
}
|
||||
|
||||
// Default: use IP address
|
||||
const ip = request.ip || request.socket.remoteAddress || 'unknown';
|
||||
return `rate-limit:${ip}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request should be rate limited
|
||||
*/
|
||||
function checkRateLimit(
|
||||
key: string,
|
||||
maxRequests: number,
|
||||
windowMs: number
|
||||
): { allowed: boolean; remaining: number; resetTime: number } {
|
||||
const now = Date.now();
|
||||
const entry = store[key];
|
||||
|
||||
// Clean expired entries periodically
|
||||
if (Math.random() < 0.01) {
|
||||
// 1% chance to clean up
|
||||
Object.keys(store).forEach((k) => {
|
||||
if (store[k]!.resetTime < now) {
|
||||
delete store[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!entry || entry.resetTime < now) {
|
||||
// Create new window
|
||||
store[key] = {
|
||||
count: 1,
|
||||
resetTime: now + windowMs,
|
||||
};
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: maxRequests - 1,
|
||||
resetTime: now + windowMs,
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.count >= maxRequests) {
|
||||
return {
|
||||
allowed: false,
|
||||
remaining: 0,
|
||||
resetTime: entry.resetTime,
|
||||
};
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: maxRequests - entry.count,
|
||||
resetTime: entry.resetTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting middleware
|
||||
*/
|
||||
export async function rateLimit(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
options: RateLimitOptions = {}
|
||||
): Promise<void> {
|
||||
const {
|
||||
maxRequests = 100,
|
||||
windowMs = 60000, // 1 minute default
|
||||
skipOnError = true,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const key = generateKey(request, options);
|
||||
const result = checkRateLimit(key, maxRequests, windowMs);
|
||||
|
||||
// Set rate limit headers
|
||||
reply.header('X-RateLimit-Limit', maxRequests.toString());
|
||||
reply.header('X-RateLimit-Remaining', result.remaining.toString());
|
||||
reply.header('X-RateLimit-Reset', new Date(result.resetTime).toISOString());
|
||||
|
||||
if (!result.allowed) {
|
||||
logger.warn({ key, ip: request.ip }, 'Rate limit exceeded');
|
||||
reply.code(429).send({
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
if (skipOnError) {
|
||||
logger.warn({ err }, 'Rate limit check failed, allowing request');
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register global rate limiting plugin
|
||||
*/
|
||||
export async function registerGlobalRateLimit(
|
||||
server: FastifyInstance,
|
||||
options: RateLimitOptions = {}
|
||||
): Promise<void> {
|
||||
server.addHook('onRequest', async (request, reply) => {
|
||||
await rateLimit(request, reply, options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limiting
|
||||
*/
|
||||
export function createUserRateLimit(maxRequests: number, windowMs: number) {
|
||||
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
||||
const userId = (request as any).user?.id || (request as any).user?.sub;
|
||||
if (!userId) {
|
||||
return; // No user, skip rate limiting
|
||||
}
|
||||
|
||||
await rateLimit(request, reply, {
|
||||
maxRequests,
|
||||
windowMs,
|
||||
keyGenerator: () => `rate-limit:user:${userId}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-IP rate limiting
|
||||
*/
|
||||
export function createIPRateLimit(maxRequests: number, windowMs: number) {
|
||||
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
||||
await rateLimit(request, reply, {
|
||||
maxRequests,
|
||||
windowMs,
|
||||
keyGenerator: (req) => `rate-limit:ip:${req.ip || req.socket.remoteAddress || 'unknown'}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user