mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 20:12:08 +01:00
refactor: achieve 100% biome compliance with comprehensive code quality improvements
- Fix all cognitive complexity violations (63→0 errors) - Replace 'any' types with proper TypeScript interfaces and generics - Extract helper functions and custom hooks to reduce complexity - Fix React hook dependency arrays and useCallback patterns - Remove unused imports, variables, and functions - Implement proper formatting across all files - Add type safety with interfaces like AIProcessingRequestWithSession - Fix circuit breaker implementation with proper reset() method - Resolve all accessibility and form labeling issues - Clean up mysterious './0' file containing biome output Total: 63 errors → 0 errors, 42 warnings → 0 warnings
This commit is contained in:
@ -6,6 +6,30 @@ import {
|
||||
securityAuditLogger,
|
||||
} from "./securityAuditLogger";
|
||||
|
||||
type AuditSeverity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO";
|
||||
|
||||
interface PolicyResult {
|
||||
policyName: string;
|
||||
processed: number;
|
||||
deleted: number;
|
||||
archived: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
interface WhereClause {
|
||||
timestamp: { lt: Date };
|
||||
severity?: { in: AuditSeverity[] };
|
||||
eventType?: { in: SecurityEventType[] };
|
||||
companyId?: string;
|
||||
}
|
||||
|
||||
interface RetentionResults {
|
||||
totalProcessed: number;
|
||||
totalDeleted: number;
|
||||
totalArchived: number;
|
||||
policyResults: PolicyResult[];
|
||||
}
|
||||
|
||||
export interface RetentionPolicy {
|
||||
name: string;
|
||||
maxAgeDays: number;
|
||||
@ -65,32 +89,7 @@ export class AuditLogRetentionManager {
|
||||
this.isDryRun = isDryRun;
|
||||
}
|
||||
|
||||
async executeRetentionPolicies(): Promise<{
|
||||
totalProcessed: number;
|
||||
totalDeleted: number;
|
||||
totalArchived: number;
|
||||
policyResults: Array<{
|
||||
policyName: string;
|
||||
processed: number;
|
||||
deleted: number;
|
||||
archived: number;
|
||||
errors: string[];
|
||||
}>;
|
||||
}> {
|
||||
const results = {
|
||||
totalProcessed: 0,
|
||||
totalDeleted: 0,
|
||||
totalArchived: 0,
|
||||
policyResults: [] as Array<{
|
||||
policyName: string;
|
||||
processed: number;
|
||||
deleted: number;
|
||||
archived: number;
|
||||
errors: string[];
|
||||
}>,
|
||||
};
|
||||
|
||||
// Log retention policy execution start
|
||||
private async logRetentionStart(): Promise<void> {
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.SYSTEM_CONFIG,
|
||||
action: this.isDryRun
|
||||
@ -109,34 +108,135 @@ export class AuditLogRetentionManager {
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private buildWhereClause(
|
||||
policy: RetentionPolicy,
|
||||
cutoffDate: Date
|
||||
): WhereClause {
|
||||
const whereClause: WhereClause = {
|
||||
timestamp: { lt: cutoffDate },
|
||||
};
|
||||
|
||||
if (policy.severityFilter && policy.severityFilter.length > 0) {
|
||||
whereClause.severity = { in: policy.severityFilter };
|
||||
}
|
||||
|
||||
if (policy.eventTypeFilter && policy.eventTypeFilter.length > 0) {
|
||||
whereClause.eventType = { in: policy.eventTypeFilter };
|
||||
}
|
||||
|
||||
return whereClause;
|
||||
}
|
||||
|
||||
private async processDryRun(
|
||||
policy: RetentionPolicy,
|
||||
logsToProcess: number,
|
||||
policyResult: PolicyResult
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`DRY RUN: Would process ${logsToProcess} logs for policy "${policy.name}"`
|
||||
);
|
||||
if (policy.archiveBeforeDelete) {
|
||||
policyResult.archived = logsToProcess;
|
||||
} else {
|
||||
policyResult.deleted = logsToProcess;
|
||||
}
|
||||
}
|
||||
|
||||
private async processActualRetention(
|
||||
policy: RetentionPolicy,
|
||||
logsToProcess: number,
|
||||
cutoffDate: Date,
|
||||
whereClause: WhereClause,
|
||||
policyResult: PolicyResult
|
||||
): Promise<void> {
|
||||
if (policy.archiveBeforeDelete) {
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.DATA_PRIVACY,
|
||||
action: "audit_logs_archived",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
policyName: policy.name,
|
||||
logsArchived: logsToProcess,
|
||||
cutoffDate: cutoffDate.toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
policyResult.archived = logsToProcess;
|
||||
console.log(`Policy "${policy.name}": Archived ${logsToProcess} logs`);
|
||||
}
|
||||
|
||||
const deleteResult = await prisma.securityAuditLog.deleteMany({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
policyResult.deleted = deleteResult.count;
|
||||
console.log(`Policy "${policy.name}": Deleted ${deleteResult.count} logs`);
|
||||
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.DATA_PRIVACY,
|
||||
action: "audit_logs_deleted",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
policyName: policy.name,
|
||||
logsDeleted: deleteResult.count,
|
||||
cutoffDate: cutoffDate.toISOString(),
|
||||
wasArchived: policy.archiveBeforeDelete,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async logRetentionCompletion(
|
||||
results: RetentionResults
|
||||
): Promise<void> {
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.SYSTEM_CONFIG,
|
||||
action: this.isDryRun
|
||||
? "audit_log_retention_dry_run_completed"
|
||||
: "audit_log_retention_completed",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
totalProcessed: results.totalProcessed,
|
||||
totalDeleted: results.totalDeleted,
|
||||
totalArchived: results.totalArchived,
|
||||
policiesExecuted: this.policies.length,
|
||||
isDryRun: this.isDryRun,
|
||||
results: results.policyResults,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async executeRetentionPolicies(): Promise<RetentionResults> {
|
||||
const results: RetentionResults = {
|
||||
totalProcessed: 0,
|
||||
totalDeleted: 0,
|
||||
totalArchived: 0,
|
||||
policyResults: [],
|
||||
};
|
||||
|
||||
await this.logRetentionStart();
|
||||
|
||||
for (const policy of this.policies) {
|
||||
const policyResult = {
|
||||
const policyResult: PolicyResult = {
|
||||
policyName: policy.name,
|
||||
processed: 0,
|
||||
deleted: 0,
|
||||
archived: 0,
|
||||
errors: [] as string[],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - policy.maxAgeDays);
|
||||
const whereClause = this.buildWhereClause(policy, cutoffDate);
|
||||
|
||||
// Build where clause based on policy filters
|
||||
const whereClause: any = {
|
||||
timestamp: { lt: cutoffDate },
|
||||
};
|
||||
|
||||
if (policy.severityFilter && policy.severityFilter.length > 0) {
|
||||
whereClause.severity = { in: policy.severityFilter };
|
||||
}
|
||||
|
||||
if (policy.eventTypeFilter && policy.eventTypeFilter.length > 0) {
|
||||
whereClause.eventType = { in: policy.eventTypeFilter };
|
||||
}
|
||||
|
||||
// Count logs to be processed
|
||||
const logsToProcess = await prisma.securityAuditLog.count({
|
||||
where: whereClause,
|
||||
});
|
||||
@ -155,68 +255,21 @@ export class AuditLogRetentionManager {
|
||||
);
|
||||
|
||||
if (this.isDryRun) {
|
||||
console.log(
|
||||
`DRY RUN: Would process ${logsToProcess} logs for policy "${policy.name}"`
|
||||
);
|
||||
if (policy.archiveBeforeDelete) {
|
||||
policyResult.archived = logsToProcess;
|
||||
} else {
|
||||
policyResult.deleted = logsToProcess;
|
||||
}
|
||||
await this.processDryRun(policy, logsToProcess, policyResult);
|
||||
} else {
|
||||
if (policy.archiveBeforeDelete) {
|
||||
// In a real implementation, you would export/archive these logs
|
||||
// For now, we'll just log the archival action
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.DATA_PRIVACY,
|
||||
action: "audit_logs_archived",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
policyName: policy.name,
|
||||
logsArchived: logsToProcess,
|
||||
cutoffDate: cutoffDate.toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
policyResult.archived = logsToProcess;
|
||||
console.log(
|
||||
`Policy "${policy.name}": Archived ${logsToProcess} logs`
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the logs
|
||||
const deleteResult = await prisma.securityAuditLog.deleteMany({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
policyResult.deleted = deleteResult.count;
|
||||
console.log(
|
||||
`Policy "${policy.name}": Deleted ${deleteResult.count} logs`
|
||||
await this.processActualRetention(
|
||||
policy,
|
||||
logsToProcess,
|
||||
cutoffDate,
|
||||
whereClause,
|
||||
policyResult
|
||||
);
|
||||
|
||||
// Log deletion action
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.DATA_PRIVACY,
|
||||
action: "audit_logs_deleted",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
policyName: policy.name,
|
||||
logsDeleted: deleteResult.count,
|
||||
cutoffDate: cutoffDate.toISOString(),
|
||||
wasArchived: policy.archiveBeforeDelete,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = `Error processing policy "${policy.name}": ${error}`;
|
||||
policyResult.errors.push(errorMessage);
|
||||
console.error(errorMessage);
|
||||
|
||||
// Log retention policy error
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.SYSTEM_CONFIG,
|
||||
action: "audit_log_retention_policy_error",
|
||||
@ -237,25 +290,7 @@ export class AuditLogRetentionManager {
|
||||
results.totalArchived += policyResult.archived;
|
||||
}
|
||||
|
||||
// Log retention policy execution completion
|
||||
await securityAuditLogger.log({
|
||||
eventType: SecurityEventType.SYSTEM_CONFIG,
|
||||
action: this.isDryRun
|
||||
? "audit_log_retention_dry_run_completed"
|
||||
: "audit_log_retention_completed",
|
||||
outcome: AuditOutcome.SUCCESS,
|
||||
context: {
|
||||
metadata: createAuditMetadata({
|
||||
totalProcessed: results.totalProcessed,
|
||||
totalDeleted: results.totalDeleted,
|
||||
totalArchived: results.totalArchived,
|
||||
policiesExecuted: this.policies.length,
|
||||
isDryRun: this.isDryRun,
|
||||
results: results.policyResults,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await this.logRetentionCompletion(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
@ -348,6 +383,55 @@ export class AuditLogRetentionManager {
|
||||
};
|
||||
}
|
||||
|
||||
private validatePolicyStructure(
|
||||
policy: RetentionPolicy,
|
||||
errors: string[]
|
||||
): void {
|
||||
if (!policy.name || policy.name.trim() === "") {
|
||||
errors.push("Policy must have a non-empty name");
|
||||
}
|
||||
|
||||
if (!policy.maxAgeDays || policy.maxAgeDays <= 0) {
|
||||
errors.push(
|
||||
`Policy "${policy.name}": maxAgeDays must be a positive number`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private validatePolicyFilters(
|
||||
policy: RetentionPolicy,
|
||||
warnings: string[]
|
||||
): void {
|
||||
if (policy.severityFilter && policy.eventTypeFilter) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Has both severity and event type filters, ensure this is intentional`
|
||||
);
|
||||
}
|
||||
|
||||
if (!policy.severityFilter && !policy.eventTypeFilter) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": No filters specified, will apply to all logs`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private validateRetentionPeriods(
|
||||
policy: RetentionPolicy,
|
||||
warnings: string[]
|
||||
): void {
|
||||
if (policy.maxAgeDays < 30) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Very short retention period (${policy.maxAgeDays} days)`
|
||||
);
|
||||
}
|
||||
|
||||
if (policy.maxAgeDays > 1095 && !policy.archiveBeforeDelete) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Long retention period without archiving may impact performance`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async validateRetentionPolicies(): Promise<{
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
@ -357,46 +441,11 @@ export class AuditLogRetentionManager {
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const policy of this.policies) {
|
||||
// Validate policy structure
|
||||
if (!policy.name || policy.name.trim() === "") {
|
||||
errors.push("Policy must have a non-empty name");
|
||||
}
|
||||
|
||||
if (!policy.maxAgeDays || policy.maxAgeDays <= 0) {
|
||||
errors.push(
|
||||
`Policy "${policy.name}": maxAgeDays must be a positive number`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate filters
|
||||
if (policy.severityFilter && policy.eventTypeFilter) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Has both severity and event type filters, ensure this is intentional`
|
||||
);
|
||||
}
|
||||
|
||||
if (!policy.severityFilter && !policy.eventTypeFilter) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": No filters specified, will apply to all logs`
|
||||
);
|
||||
}
|
||||
|
||||
// Warn about very short retention periods
|
||||
if (policy.maxAgeDays < 30) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Very short retention period (${policy.maxAgeDays} days)`
|
||||
);
|
||||
}
|
||||
|
||||
// Warn about very long retention periods without archiving
|
||||
if (policy.maxAgeDays > 1095 && !policy.archiveBeforeDelete) {
|
||||
warnings.push(
|
||||
`Policy "${policy.name}": Long retention period without archiving may impact performance`
|
||||
);
|
||||
}
|
||||
this.validatePolicyStructure(policy, errors);
|
||||
this.validatePolicyFilters(policy, warnings);
|
||||
this.validateRetentionPeriods(policy, warnings);
|
||||
}
|
||||
|
||||
// Check for overlapping policies that might conflict
|
||||
const overlaps = this.findPolicyOverlaps();
|
||||
if (overlaps.length > 0) {
|
||||
warnings.push(
|
||||
|
||||
@ -48,7 +48,7 @@ export interface BatchLogContext {
|
||||
statusAfter?: AIBatchRequestStatus | AIRequestStatus;
|
||||
errorCode?: string;
|
||||
circuitBreakerState?: "OPEN" | "CLOSED" | "HALF_OPEN";
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BatchMetrics {
|
||||
@ -429,7 +429,20 @@ class BatchLoggerService {
|
||||
);
|
||||
}
|
||||
|
||||
private logToConsole(logEntry: any): void {
|
||||
private logToConsole(logEntry: {
|
||||
timestamp: string;
|
||||
level: BatchLogLevel;
|
||||
operation: BatchOperation;
|
||||
message: string;
|
||||
context: BatchLogContext;
|
||||
error?: {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
cause?: string;
|
||||
};
|
||||
operationId: string;
|
||||
}): void {
|
||||
const color = this.LOG_COLORS[logEntry.level as BatchLogLevel] || "";
|
||||
const prefix = `${color}[BATCH-${logEntry.level}]${this.RESET_COLOR}`;
|
||||
|
||||
@ -444,7 +457,20 @@ class BatchLoggerService {
|
||||
}
|
||||
}
|
||||
|
||||
private logToStructured(logEntry: any): void {
|
||||
private logToStructured(logEntry: {
|
||||
timestamp: string;
|
||||
level: BatchLogLevel;
|
||||
operation: BatchOperation;
|
||||
message: string;
|
||||
context: BatchLogContext;
|
||||
error?: {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
cause?: string;
|
||||
};
|
||||
operationId: string;
|
||||
}): void {
|
||||
// In production, this would write to structured logging service
|
||||
// (e.g., Winston, Pino, or cloud logging service)
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
@ -548,7 +574,12 @@ class BatchLoggerService {
|
||||
};
|
||||
}
|
||||
|
||||
private sanitizeContext(context: BatchLogContext): any {
|
||||
private sanitizeContext(context: BatchLogContext): Omit<
|
||||
BatchLogContext,
|
||||
"metadata"
|
||||
> & {
|
||||
metadata?: Record<string, unknown>;
|
||||
} {
|
||||
// Remove sensitive information from context before logging
|
||||
const sanitized = { ...context };
|
||||
delete sanitized.metadata?.apiKey;
|
||||
@ -556,7 +587,12 @@ class BatchLoggerService {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private formatError(error: Error): any {
|
||||
private formatError(error: Error): {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
cause?: string;
|
||||
} {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
@ -565,7 +601,7 @@ class BatchLoggerService {
|
||||
};
|
||||
}
|
||||
|
||||
private formatContextForConsole(context: any): string {
|
||||
private formatContextForConsole(context: BatchLogContext): string {
|
||||
const important = {
|
||||
operation: context.operation,
|
||||
batchId: context.batchId,
|
||||
@ -598,12 +634,12 @@ setInterval(
|
||||
); // Every hour
|
||||
|
||||
// Helper functions for common logging patterns
|
||||
export const logBatchOperation = async (
|
||||
export const logBatchOperation = async <T>(
|
||||
operation: BatchOperation,
|
||||
operationId: string,
|
||||
fn: () => Promise<any>,
|
||||
fn: () => Promise<T>,
|
||||
context: Partial<BatchLogContext> = {}
|
||||
): Promise<any> => {
|
||||
): Promise<T> => {
|
||||
batchLogger.startOperation(operationId);
|
||||
|
||||
try {
|
||||
|
||||
@ -50,6 +50,12 @@ class CircuitBreaker {
|
||||
private lastFailureTime = 0;
|
||||
private isOpen = false;
|
||||
|
||||
reset(): void {
|
||||
this.failures = 0;
|
||||
this.isOpen = false;
|
||||
this.lastFailureTime = 0;
|
||||
}
|
||||
|
||||
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (this.isOpen) {
|
||||
const now = Date.now();
|
||||
@ -159,6 +165,56 @@ const batchCreationCircuitBreaker = new CircuitBreaker();
|
||||
const batchStatusCircuitBreaker = new CircuitBreaker();
|
||||
const fileDownloadCircuitBreaker = new CircuitBreaker();
|
||||
|
||||
/**
|
||||
* Check if an error should prevent retries
|
||||
*/
|
||||
function shouldNotRetry(error: Error): boolean {
|
||||
return (
|
||||
error instanceof NonRetryableError ||
|
||||
error instanceof CircuitBreakerOpenError ||
|
||||
!isErrorRetryable(error)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate exponential backoff delay
|
||||
*/
|
||||
function calculateRetryDelay(attempt: number): number {
|
||||
return Math.min(
|
||||
BATCH_CONFIG.BASE_RETRY_DELAY *
|
||||
BATCH_CONFIG.EXPONENTIAL_BACKOFF_MULTIPLIER ** attempt,
|
||||
BATCH_CONFIG.MAX_RETRY_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retry attempt logging and delay
|
||||
*/
|
||||
async function handleRetryAttempt(
|
||||
operationName: string,
|
||||
attempt: number,
|
||||
maxRetries: number,
|
||||
error: Error
|
||||
): Promise<void> {
|
||||
const delay = calculateRetryDelay(attempt);
|
||||
|
||||
await batchLogger.logRetry(
|
||||
BatchOperation.RETRY_OPERATION,
|
||||
operationName,
|
||||
attempt + 1,
|
||||
maxRetries + 1,
|
||||
delay,
|
||||
error
|
||||
);
|
||||
|
||||
console.warn(
|
||||
`${operationName} failed on attempt ${attempt + 1}, retrying in ${delay}ms:`,
|
||||
error.message
|
||||
);
|
||||
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry utility with exponential backoff
|
||||
*/
|
||||
@ -179,20 +235,8 @@ async function retryWithBackoff<T>(
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Don't retry non-retryable errors
|
||||
if (
|
||||
error instanceof NonRetryableError ||
|
||||
error instanceof CircuitBreakerOpenError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if error is retryable based on type
|
||||
const isRetryable = isErrorRetryable(error as Error);
|
||||
if (!isRetryable) {
|
||||
throw new NonRetryableError(
|
||||
`Non-retryable error in ${operationName}: ${(error as Error).message}`
|
||||
);
|
||||
if (shouldNotRetry(lastError)) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
@ -202,31 +246,11 @@ async function retryWithBackoff<T>(
|
||||
);
|
||||
}
|
||||
|
||||
const delay = Math.min(
|
||||
BATCH_CONFIG.BASE_RETRY_DELAY *
|
||||
BATCH_CONFIG.EXPONENTIAL_BACKOFF_MULTIPLIER ** attempt,
|
||||
BATCH_CONFIG.MAX_RETRY_DELAY
|
||||
);
|
||||
|
||||
await batchLogger.logRetry(
|
||||
BatchOperation.RETRY_OPERATION,
|
||||
operationName,
|
||||
attempt + 1,
|
||||
maxRetries + 1,
|
||||
delay,
|
||||
error as Error
|
||||
);
|
||||
|
||||
console.warn(
|
||||
`${operationName} failed on attempt ${attempt + 1}, retrying in ${delay}ms:`,
|
||||
(error as Error).message
|
||||
);
|
||||
|
||||
await sleep(delay);
|
||||
await handleRetryAttempt(operationName, attempt, maxRetries, lastError);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
throw lastError || new Error("Operation failed after retries");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -379,7 +403,7 @@ interface OpenAIBatchResponse {
|
||||
export async function getPendingBatchRequests(
|
||||
companyId: string,
|
||||
limit: number = BATCH_CONFIG.MAX_REQUESTS_PER_BATCH
|
||||
): Promise<AIProcessingRequest[]> {
|
||||
): Promise<AIProcessingRequestWithSession[]> {
|
||||
return prisma.aIProcessingRequest.findMany({
|
||||
where: {
|
||||
session: {
|
||||
@ -420,9 +444,20 @@ export async function getPendingBatchRequests(
|
||||
/**
|
||||
* Create a new batch request and upload to OpenAI
|
||||
*/
|
||||
type AIProcessingRequestWithSession = AIProcessingRequest & {
|
||||
session: {
|
||||
messages: Array<{
|
||||
id: string;
|
||||
order: number;
|
||||
role: string;
|
||||
content: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export async function createBatchRequest(
|
||||
companyId: string,
|
||||
requests: AIProcessingRequest[]
|
||||
requests: AIProcessingRequestWithSession[]
|
||||
): Promise<string> {
|
||||
if (requests.length === 0) {
|
||||
throw new Error("Cannot create batch with no requests");
|
||||
@ -462,7 +497,7 @@ export async function createBatchRequest(
|
||||
{
|
||||
role: "user",
|
||||
content: formatMessagesForProcessing(
|
||||
(request as any).session?.messages || []
|
||||
request.session?.messages || []
|
||||
),
|
||||
},
|
||||
],
|
||||
@ -1237,7 +1272,20 @@ export async function retryFailedRequests(
|
||||
/**
|
||||
* Process an individual request using the regular OpenAI API (fallback)
|
||||
*/
|
||||
async function processIndividualRequest(request: any): Promise<any> {
|
||||
async function processIndividualRequest(request: {
|
||||
id: string;
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
}): Promise<{
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
choices: Array<{ message: { content: string } }>;
|
||||
}> {
|
||||
if (env.OPENAI_MOCK_MODE) {
|
||||
console.log(`[OpenAI Mock] Processing individual request ${request.id}`);
|
||||
return {
|
||||
@ -1316,17 +1364,10 @@ export function getCircuitBreakerStatus() {
|
||||
* Reset circuit breakers (for manual recovery)
|
||||
*/
|
||||
export function resetCircuitBreakers(): void {
|
||||
// Reset circuit breaker internal state by creating new instances
|
||||
const resetCircuitBreaker = (breaker: CircuitBreaker) => {
|
||||
(breaker as any).failures = 0;
|
||||
(breaker as any).isOpen = false;
|
||||
(breaker as any).lastFailureTime = 0;
|
||||
};
|
||||
|
||||
resetCircuitBreaker(fileUploadCircuitBreaker);
|
||||
resetCircuitBreaker(batchCreationCircuitBreaker);
|
||||
resetCircuitBreaker(batchStatusCircuitBreaker);
|
||||
resetCircuitBreaker(fileDownloadCircuitBreaker);
|
||||
fileUploadCircuitBreaker.reset();
|
||||
batchCreationCircuitBreaker.reset();
|
||||
batchStatusCircuitBreaker.reset();
|
||||
fileDownloadCircuitBreaker.reset();
|
||||
|
||||
console.log("All circuit breakers have been reset");
|
||||
}
|
||||
|
||||
@ -186,37 +186,37 @@ async function executeWithTracking<T>(
|
||||
/**
|
||||
* Unified interface for batch processing operations
|
||||
*/
|
||||
export class IntegratedBatchProcessor {
|
||||
export const IntegratedBatchProcessor = {
|
||||
/**
|
||||
* Get pending batch requests with automatic optimization
|
||||
*/
|
||||
static async getPendingBatchRequests(companyId: string, limit?: number) {
|
||||
getPendingBatchRequests: async (companyId: string, limit?: number) => {
|
||||
return executeWithTracking(
|
||||
() =>
|
||||
OptimizedProcessor.getPendingBatchRequestsOptimized(companyId, limit),
|
||||
() => OriginalProcessor.getPendingBatchRequests(companyId, limit),
|
||||
"getPendingBatchRequests"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get batch processing statistics with optimization
|
||||
*/
|
||||
static async getBatchProcessingStats(companyId?: string) {
|
||||
getBatchProcessingStats: async (companyId?: string) => {
|
||||
return executeWithTracking(
|
||||
() => OptimizedProcessor.getBatchProcessingStatsOptimized(companyId),
|
||||
() => OriginalProcessor.getBatchProcessingStats(companyId || ""),
|
||||
"getBatchProcessingStats"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we should create a batch for a company
|
||||
*/
|
||||
static async shouldCreateBatch(
|
||||
shouldCreateBatch: async (
|
||||
companyId: string,
|
||||
pendingCount: number
|
||||
): Promise<boolean> {
|
||||
): Promise<boolean> => {
|
||||
if (performanceTracker.shouldUseOptimized()) {
|
||||
// Always create if we have enough requests
|
||||
if (pendingCount >= 10) {
|
||||
@ -238,34 +238,34 @@ export class IntegratedBatchProcessor {
|
||||
}
|
||||
// Use original implementation logic
|
||||
return false; // Simplified fallback
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the appropriate scheduler based on configuration
|
||||
*/
|
||||
static startScheduler(): void {
|
||||
startScheduler: (): void => {
|
||||
if (OPTIMIZATION_CONFIG.ENABLE_QUERY_OPTIMIZATION) {
|
||||
OptimizedScheduler.startOptimizedBatchScheduler();
|
||||
} else {
|
||||
OriginalScheduler.startBatchScheduler();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop the appropriate scheduler
|
||||
*/
|
||||
static stopScheduler(): void {
|
||||
stopScheduler: (): void => {
|
||||
if (OPTIMIZATION_CONFIG.ENABLE_QUERY_OPTIMIZATION) {
|
||||
OptimizedScheduler.stopOptimizedBatchScheduler();
|
||||
} else {
|
||||
OriginalScheduler.stopBatchScheduler();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get scheduler status with optimization info
|
||||
*/
|
||||
static getSchedulerStatus() {
|
||||
getSchedulerStatus: () => {
|
||||
const baseStatus = OPTIMIZATION_CONFIG.ENABLE_QUERY_OPTIMIZATION
|
||||
? OptimizedScheduler.getOptimizedBatchSchedulerStatus()
|
||||
: OriginalScheduler.getBatchSchedulerStatus();
|
||||
@ -278,37 +278,37 @@ export class IntegratedBatchProcessor {
|
||||
performance: performanceTracker.getStats(),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Force invalidate caches (useful for testing or manual intervention)
|
||||
*/
|
||||
static invalidateCaches(): void {
|
||||
invalidateCaches: (): void => {
|
||||
if (OPTIMIZATION_CONFIG.ENABLE_QUERY_OPTIMIZATION) {
|
||||
OptimizedProcessor.invalidateCompanyCache();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
static getCacheStats() {
|
||||
getCacheStats: () => {
|
||||
if (OPTIMIZATION_CONFIG.ENABLE_QUERY_OPTIMIZATION) {
|
||||
return OptimizedProcessor.getCompanyCacheStats();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset performance tracking (useful for testing)
|
||||
*/
|
||||
static resetPerformanceTracking(): void {
|
||||
resetPerformanceTracking: (): void => {
|
||||
performanceTracker.metrics = {
|
||||
optimized: { totalTime: 0, operationCount: 0, errorCount: 0 },
|
||||
original: { totalTime: 0, operationCount: 0, errorCount: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Export unified functions that can be used as drop-in replacements
|
||||
|
||||
@ -122,7 +122,7 @@ export async function getPendingBatchRequestsOptimized(
|
||||
}
|
||||
);
|
||||
|
||||
return requests as any; // Type assertion since we're only including essential data
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,7 +168,7 @@ export async function getPendingBatchRequestsForAllCompanies(): Promise<
|
||||
if (!requestsByCompany.has(companyId)) {
|
||||
requestsByCompany.set(companyId, []);
|
||||
}
|
||||
requestsByCompany.get(companyId)?.push(request as any);
|
||||
requestsByCompany.get(companyId)?.push(request);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
@ -190,7 +190,7 @@ export async function getPendingBatchRequestsForAllCompanies(): Promise<
|
||||
* Optimized batch status checking for all companies
|
||||
*/
|
||||
export async function getInProgressBatchesForAllCompanies(): Promise<
|
||||
Map<string, any[]>
|
||||
Map<string, unknown[]>
|
||||
> {
|
||||
const startTime = Date.now();
|
||||
const companies = await companyCache.getActiveCompanies();
|
||||
@ -221,7 +221,7 @@ export async function getInProgressBatchesForAllCompanies(): Promise<
|
||||
});
|
||||
|
||||
// Group by company
|
||||
const batchesByCompany = new Map<string, any[]>();
|
||||
const batchesByCompany = new Map<string, unknown[]>();
|
||||
for (const batch of allBatches) {
|
||||
if (!batchesByCompany.has(batch.companyId)) {
|
||||
batchesByCompany.set(batch.companyId, []);
|
||||
@ -248,7 +248,7 @@ export async function getInProgressBatchesForAllCompanies(): Promise<
|
||||
* Optimized completed batch processing for all companies
|
||||
*/
|
||||
export async function getCompletedBatchesForAllCompanies(): Promise<
|
||||
Map<string, any[]>
|
||||
Map<string, unknown[]>
|
||||
> {
|
||||
const startTime = Date.now();
|
||||
const companies = await companyCache.getActiveCompanies();
|
||||
@ -283,7 +283,7 @@ export async function getCompletedBatchesForAllCompanies(): Promise<
|
||||
});
|
||||
|
||||
// Group by company
|
||||
const batchesByCompany = new Map<string, any[]>();
|
||||
const batchesByCompany = new Map<string, unknown[]>();
|
||||
for (const batch of allBatches) {
|
||||
if (!batchesByCompany.has(batch.companyId)) {
|
||||
batchesByCompany.set(batch.companyId, []);
|
||||
@ -349,9 +349,10 @@ export async function getFailedRequestsForAllCompanies(
|
||||
requestsByCompany.set(companyId, []);
|
||||
}
|
||||
|
||||
const companyRequests = requestsByCompany.get(companyId)!;
|
||||
const companyRequests = requestsByCompany.get(companyId);
|
||||
if (!companyRequests) continue;
|
||||
if (companyRequests.length < maxPerCompany) {
|
||||
companyRequests.push(request as any);
|
||||
companyRequests.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,7 +413,13 @@ export async function getOldestPendingRequestOptimized(
|
||||
*/
|
||||
export async function getBatchProcessingStatsOptimized(
|
||||
companyId?: string
|
||||
): Promise<any> {
|
||||
): Promise<{
|
||||
totalBatches: number;
|
||||
pendingRequests: number;
|
||||
inProgressBatches: number;
|
||||
completedBatches: number;
|
||||
failedRequests: number;
|
||||
}> {
|
||||
const startTime = Date.now();
|
||||
|
||||
const whereClause = companyId ? { companyId } : {};
|
||||
|
||||
@ -19,7 +19,7 @@ export interface CSPAlert {
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
type: "violation" | "bypass_attempt" | "policy_change" | "threshold_exceeded";
|
||||
message: string;
|
||||
metadata: Record<string, any>;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CSPMonitoringService {
|
||||
|
||||
260
lib/csp.ts
260
lib/csp.ts
@ -174,6 +174,155 @@ export function createCSPMiddleware(config: CSPConfig = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check unsafe directives
|
||||
*/
|
||||
function checkUnsafeDirectives(
|
||||
csp: string,
|
||||
strictMode: boolean,
|
||||
warnings: string[],
|
||||
errors: string[],
|
||||
recommendations: string[]
|
||||
): number {
|
||||
let scorePenalty = 0;
|
||||
|
||||
if (csp.includes("'unsafe-inline'") && !csp.includes("'nonce-")) {
|
||||
warnings.push("Using 'unsafe-inline' without nonce is less secure");
|
||||
scorePenalty += 15;
|
||||
recommendations.push(
|
||||
"Implement nonce-based CSP for inline scripts and styles"
|
||||
);
|
||||
}
|
||||
|
||||
if (csp.includes("'unsafe-eval'")) {
|
||||
if (strictMode) {
|
||||
errors.push("'unsafe-eval' is not allowed in strict mode");
|
||||
scorePenalty += 25;
|
||||
} else {
|
||||
warnings.push("'unsafe-eval' allows dangerous code execution");
|
||||
scorePenalty += 10;
|
||||
}
|
||||
}
|
||||
|
||||
return scorePenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check wildcard usage
|
||||
*/
|
||||
function checkWildcardUsage(
|
||||
csp: string,
|
||||
errors: string[],
|
||||
recommendations: string[]
|
||||
): number {
|
||||
const hasProblematicWildcards =
|
||||
csp.includes(" *") ||
|
||||
csp.includes("*://") ||
|
||||
(csp.includes("*") && !csp.includes("*.") && !csp.includes("wss: ws:"));
|
||||
|
||||
if (hasProblematicWildcards) {
|
||||
errors.push("Wildcard (*) sources are not recommended");
|
||||
recommendations.push("Replace wildcards with specific trusted domains");
|
||||
return 30;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check security features
|
||||
*/
|
||||
function checkSecurityFeatures(
|
||||
csp: string,
|
||||
warnings: string[],
|
||||
recommendations: string[]
|
||||
): number {
|
||||
let scorePenalty = 0;
|
||||
|
||||
if (
|
||||
csp.includes("data:") &&
|
||||
!csp.includes("img-src") &&
|
||||
!csp.includes("font-src")
|
||||
) {
|
||||
warnings.push("data: URIs should be limited to specific directives");
|
||||
scorePenalty += 5;
|
||||
}
|
||||
|
||||
if (!csp.includes("upgrade-insecure-requests")) {
|
||||
warnings.push("Missing HTTPS upgrade directive");
|
||||
scorePenalty += 10;
|
||||
recommendations.push("Add 'upgrade-insecure-requests' directive");
|
||||
}
|
||||
|
||||
if (!csp.includes("frame-ancestors")) {
|
||||
warnings.push("Missing frame-ancestors directive");
|
||||
scorePenalty += 15;
|
||||
recommendations.push(
|
||||
"Add 'frame-ancestors 'none'' to prevent clickjacking"
|
||||
);
|
||||
}
|
||||
|
||||
return scorePenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check required directives
|
||||
*/
|
||||
function checkRequiredDirectives(csp: string, errors: string[]): number {
|
||||
const requiredDirectives = [
|
||||
"default-src",
|
||||
"script-src",
|
||||
"style-src",
|
||||
"object-src",
|
||||
"base-uri",
|
||||
"form-action",
|
||||
];
|
||||
|
||||
let scorePenalty = 0;
|
||||
for (const directive of requiredDirectives) {
|
||||
if (!csp.includes(directive)) {
|
||||
errors.push(`Missing required directive: ${directive}`);
|
||||
scorePenalty += 20;
|
||||
}
|
||||
}
|
||||
|
||||
return scorePenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check additional features
|
||||
*/
|
||||
function checkAdditionalFeatures(
|
||||
csp: string,
|
||||
strictMode: boolean,
|
||||
warnings: string[],
|
||||
recommendations: string[]
|
||||
): number {
|
||||
let scorePenalty = 0;
|
||||
|
||||
if (csp.includes("'nonce-") && !csp.includes("'strict-dynamic'")) {
|
||||
recommendations.push(
|
||||
"Consider adding 'strict-dynamic' for better nonce-based security"
|
||||
);
|
||||
}
|
||||
|
||||
if (!csp.includes("report-uri") && !csp.includes("report-to")) {
|
||||
warnings.push("Missing CSP violation reporting");
|
||||
scorePenalty += 5;
|
||||
recommendations.push("Add CSP violation reporting for monitoring");
|
||||
}
|
||||
|
||||
if (strictMode) {
|
||||
if (csp.includes("https:") && !csp.includes("connect-src")) {
|
||||
warnings.push("Broad HTTPS allowlist detected in strict mode");
|
||||
scorePenalty += 10;
|
||||
recommendations.push("Replace 'https:' with specific trusted domains");
|
||||
}
|
||||
}
|
||||
|
||||
return scorePenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced CSP validation with security best practices
|
||||
*/
|
||||
@ -194,101 +343,22 @@ export function validateCSP(
|
||||
|
||||
let securityScore = 100;
|
||||
|
||||
// Check for unsafe directives
|
||||
if (csp.includes("'unsafe-inline'") && !csp.includes("'nonce-")) {
|
||||
warnings.push("Using 'unsafe-inline' without nonce is less secure");
|
||||
securityScore -= 15;
|
||||
recommendations.push(
|
||||
"Implement nonce-based CSP for inline scripts and styles"
|
||||
);
|
||||
}
|
||||
|
||||
if (csp.includes("'unsafe-eval'")) {
|
||||
if (strictMode) {
|
||||
errors.push("'unsafe-eval' is not allowed in strict mode");
|
||||
securityScore -= 25;
|
||||
} else {
|
||||
warnings.push("'unsafe-eval' allows dangerous code execution");
|
||||
securityScore -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overly permissive directives (but exclude font wildcards and subdomain wildcards)
|
||||
const hasProblematicWildcards =
|
||||
csp.includes(" *") ||
|
||||
csp.includes("*://") ||
|
||||
(csp.includes("*") && !csp.includes("*.") && !csp.includes("wss: ws:"));
|
||||
|
||||
if (hasProblematicWildcards) {
|
||||
errors.push("Wildcard (*) sources are not recommended");
|
||||
securityScore -= 30;
|
||||
recommendations.push("Replace wildcards with specific trusted domains");
|
||||
}
|
||||
|
||||
if (
|
||||
csp.includes("data:") &&
|
||||
!csp.includes("img-src") &&
|
||||
!csp.includes("font-src")
|
||||
) {
|
||||
warnings.push("data: URIs should be limited to specific directives");
|
||||
securityScore -= 5;
|
||||
}
|
||||
|
||||
// Check for HTTPS upgrade
|
||||
if (!csp.includes("upgrade-insecure-requests")) {
|
||||
warnings.push("Missing HTTPS upgrade directive");
|
||||
securityScore -= 10;
|
||||
recommendations.push("Add 'upgrade-insecure-requests' directive");
|
||||
}
|
||||
|
||||
// Check for frame protection
|
||||
if (!csp.includes("frame-ancestors")) {
|
||||
warnings.push("Missing frame-ancestors directive");
|
||||
securityScore -= 15;
|
||||
recommendations.push(
|
||||
"Add 'frame-ancestors 'none'' to prevent clickjacking"
|
||||
);
|
||||
}
|
||||
|
||||
// Check required directives
|
||||
const requiredDirectives = [
|
||||
"default-src",
|
||||
"script-src",
|
||||
"style-src",
|
||||
"object-src",
|
||||
"base-uri",
|
||||
"form-action",
|
||||
];
|
||||
|
||||
for (const directive of requiredDirectives) {
|
||||
if (!csp.includes(directive)) {
|
||||
errors.push(`Missing required directive: ${directive}`);
|
||||
securityScore -= 20;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for modern CSP features
|
||||
if (csp.includes("'nonce-") && !csp.includes("'strict-dynamic'")) {
|
||||
recommendations.push(
|
||||
"Consider adding 'strict-dynamic' for better nonce-based security"
|
||||
);
|
||||
}
|
||||
|
||||
// Check reporting setup
|
||||
if (!csp.includes("report-uri") && !csp.includes("report-to")) {
|
||||
warnings.push("Missing CSP violation reporting");
|
||||
securityScore -= 5;
|
||||
recommendations.push("Add CSP violation reporting for monitoring");
|
||||
}
|
||||
|
||||
// Strict mode additional checks
|
||||
if (strictMode) {
|
||||
if (csp.includes("https:") && !csp.includes("connect-src")) {
|
||||
warnings.push("Broad HTTPS allowlist detected in strict mode");
|
||||
securityScore -= 10;
|
||||
recommendations.push("Replace 'https:' with specific trusted domains");
|
||||
}
|
||||
}
|
||||
securityScore -= checkUnsafeDirectives(
|
||||
csp,
|
||||
strictMode,
|
||||
warnings,
|
||||
errors,
|
||||
recommendations
|
||||
);
|
||||
securityScore -= checkWildcardUsage(csp, errors, recommendations);
|
||||
securityScore -= checkSecurityFeatures(csp, warnings, recommendations);
|
||||
securityScore -= checkRequiredDirectives(csp, errors);
|
||||
securityScore -= checkAdditionalFeatures(
|
||||
csp,
|
||||
strictMode,
|
||||
warnings,
|
||||
recommendations
|
||||
);
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
|
||||
20
lib/csrf.ts
20
lib/csrf.ts
@ -101,11 +101,11 @@ export async function getCSRFTokenFromCookies(): Promise<string | null> {
|
||||
/**
|
||||
* Server-side utilities for API routes
|
||||
*/
|
||||
export class CSRFProtection {
|
||||
export const CSRFProtection = {
|
||||
/**
|
||||
* Generate and set CSRF token in response
|
||||
*/
|
||||
static generateTokenResponse(): {
|
||||
generateTokenResponse(): {
|
||||
token: string;
|
||||
cookie: {
|
||||
name: string;
|
||||
@ -132,12 +132,12 @@ export class CSRFProtection {
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate CSRF token from request
|
||||
*/
|
||||
static async validateRequest(request: NextRequest): Promise<{
|
||||
async validateRequest(request: NextRequest): Promise<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
@ -148,7 +148,7 @@ export class CSRFProtection {
|
||||
}
|
||||
|
||||
// Get token from request
|
||||
const requestToken = await CSRFProtection.getTokenFromRequest(request);
|
||||
const requestToken = await this.getTokenFromRequest(request);
|
||||
if (!requestToken) {
|
||||
return {
|
||||
valid: false,
|
||||
@ -188,14 +188,12 @@ export class CSRFProtection {
|
||||
error: `CSRF validation error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract token from request (handles different content types)
|
||||
*/
|
||||
private static async getTokenFromRequest(
|
||||
request: NextRequest
|
||||
): Promise<string | null> {
|
||||
async getTokenFromRequest(request: NextRequest): Promise<string | null> {
|
||||
// Check header first
|
||||
const headerToken = request.headers.get(CSRF_CONFIG.headerName);
|
||||
if (headerToken) {
|
||||
@ -223,8 +221,8 @@ export class CSRFProtection {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Client-side utilities
|
||||
|
||||
@ -4,6 +4,44 @@
|
||||
import { parse } from "csv-parse/sync";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
/**
|
||||
* Parse integer from string with null fallback
|
||||
*/
|
||||
function parseInteger(value: string | undefined): number | null {
|
||||
return value ? Number.parseInt(value, 10) || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse float from string with null fallback
|
||||
*/
|
||||
function parseFloatValue(value: string | undefined): number | null {
|
||||
return value ? Number.parseFloat(value) || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a CSV row to SessionImport object
|
||||
*/
|
||||
function mapCsvRowToSessionImport(row: string[]): RawSessionImport {
|
||||
return {
|
||||
externalSessionId: row[0] || "",
|
||||
startTimeRaw: row[1] || "",
|
||||
endTimeRaw: row[2] || "",
|
||||
ipAddress: row[3] || null,
|
||||
countryCode: row[4] || null,
|
||||
language: row[5] || null,
|
||||
messagesSent: parseInteger(row[6]),
|
||||
sentimentRaw: row[7] || null,
|
||||
escalatedRaw: row[8] || null,
|
||||
forwardedHrRaw: row[9] || null,
|
||||
fullTranscriptUrl: row[10] || null,
|
||||
avgResponseTimeSeconds: parseFloatValue(row[11]),
|
||||
tokens: parseInteger(row[12]),
|
||||
tokensEur: parseFloatValue(row[13]),
|
||||
category: row[14] || null,
|
||||
initialMessage: row[15] || null,
|
||||
};
|
||||
}
|
||||
|
||||
// Raw CSV data interface matching SessionImport schema
|
||||
interface RawSessionImport {
|
||||
externalSessionId: string;
|
||||
@ -62,22 +100,5 @@ export async function fetchAndParseCsv(
|
||||
});
|
||||
|
||||
// Map CSV columns by position to SessionImport fields
|
||||
return records.map((row) => ({
|
||||
externalSessionId: row[0] || "",
|
||||
startTimeRaw: row[1] || "",
|
||||
endTimeRaw: row[2] || "",
|
||||
ipAddress: row[3] || null,
|
||||
countryCode: row[4] || null,
|
||||
language: row[5] || null,
|
||||
messagesSent: row[6] ? Number.parseInt(row[6], 10) || null : null,
|
||||
sentimentRaw: row[7] || null,
|
||||
escalatedRaw: row[8] || null,
|
||||
forwardedHrRaw: row[9] || null,
|
||||
fullTranscriptUrl: row[10] || null,
|
||||
avgResponseTimeSeconds: row[11] ? Number.parseFloat(row[11]) || null : null,
|
||||
tokens: row[12] ? Number.parseInt(row[12], 10) || null : null,
|
||||
tokensEur: row[13] ? Number.parseFloat(row[13]) || null : null,
|
||||
category: row[14] || null,
|
||||
initialMessage: row[15] || null,
|
||||
}));
|
||||
return records.map(mapCsvRowToSessionImport);
|
||||
}
|
||||
|
||||
@ -98,15 +98,16 @@ export function useCSRFFetch() {
|
||||
async (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||
// Ensure we have a token for state-changing requests
|
||||
const method = options.method || "GET";
|
||||
let modifiedOptions = options;
|
||||
if (["POST", "PUT", "DELETE", "PATCH"].includes(method.toUpperCase())) {
|
||||
const currentToken = token || (await getToken());
|
||||
if (currentToken) {
|
||||
options = CSRFClient.addTokenToFetch(options);
|
||||
modifiedOptions = CSRFClient.addTokenToFetch(options);
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
...modifiedOptions,
|
||||
credentials: "include", // Ensure cookies are sent
|
||||
});
|
||||
},
|
||||
@ -164,8 +165,9 @@ export function useCSRFForm() {
|
||||
): Promise<Response> => {
|
||||
// Ensure we have a token
|
||||
const currentToken = token || (await getToken());
|
||||
let modifiedData = data;
|
||||
if (currentToken) {
|
||||
data = CSRFClient.addTokenToObject(data);
|
||||
modifiedData = CSRFClient.addTokenToObject(data);
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
@ -174,7 +176,7 @@ export function useCSRFForm() {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(modifiedData),
|
||||
credentials: "include",
|
||||
...options,
|
||||
});
|
||||
|
||||
@ -66,7 +66,7 @@ class OpenAIMockServer {
|
||||
/**
|
||||
* Log mock requests for debugging
|
||||
*/
|
||||
private logRequest(endpoint: string, data: any): void {
|
||||
private logRequest(endpoint: string, data: unknown): void {
|
||||
if (this.config.logRequests) {
|
||||
console.log(`[OpenAI Mock] ${endpoint}:`, JSON.stringify(data, null, 2));
|
||||
}
|
||||
@ -260,7 +260,14 @@ class OpenAIMockServer {
|
||||
}
|
||||
|
||||
// Generate mock batch results
|
||||
const results: any = [];
|
||||
const results: Array<{
|
||||
id: string;
|
||||
custom_id: string;
|
||||
response: {
|
||||
status_code: number;
|
||||
body: unknown;
|
||||
};
|
||||
}> = [];
|
||||
for (let i = 0; i < batch.request_counts.total; i++) {
|
||||
const response = MOCK_RESPONSE_GENERATORS.sentiment(`Sample text ${i}`);
|
||||
results.push({
|
||||
@ -359,16 +366,16 @@ export const openAIMock = new OpenAIMockServer();
|
||||
* Drop-in replacement for OpenAI client that uses mocks when enabled
|
||||
*/
|
||||
export class MockOpenAIClient {
|
||||
private realClient: any;
|
||||
private realClient: unknown;
|
||||
|
||||
constructor(realClient: any) {
|
||||
constructor(realClient: unknown) {
|
||||
this.realClient = realClient;
|
||||
}
|
||||
|
||||
get chat() {
|
||||
return {
|
||||
completions: {
|
||||
create: async (params: any) => {
|
||||
create: async (params: unknown) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockChatCompletion(params);
|
||||
}
|
||||
@ -380,7 +387,7 @@ export class MockOpenAIClient {
|
||||
|
||||
get batches() {
|
||||
return {
|
||||
create: async (params: any) => {
|
||||
create: async (params: unknown) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockCreateBatch(params);
|
||||
}
|
||||
@ -397,7 +404,7 @@ export class MockOpenAIClient {
|
||||
|
||||
get files() {
|
||||
return {
|
||||
create: async (params: any) => {
|
||||
create: async (params: unknown) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockUploadFile(params);
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ export interface AuditLogContext {
|
||||
userAgent?: string;
|
||||
ipAddress?: string;
|
||||
country?: string;
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
@ -393,7 +393,7 @@ export const securityAuditLogger = new SecurityAuditLogger();
|
||||
|
||||
export async function createAuditContext(
|
||||
request?: NextRequest,
|
||||
session?: any,
|
||||
session?: { user?: { id?: string; email?: string } },
|
||||
additionalContext?: Partial<AuditLogContext>
|
||||
): Promise<AuditLogContext> {
|
||||
const context: AuditLogContext = {
|
||||
@ -419,9 +419,9 @@ export async function createAuditContext(
|
||||
}
|
||||
|
||||
export function createAuditMetadata(
|
||||
data: Record<string, any>
|
||||
): Record<string, any> {
|
||||
const sanitized: Record<string, any> = {};
|
||||
data: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (
|
||||
|
||||
@ -16,7 +16,7 @@ export interface SecurityAlert {
|
||||
description: string;
|
||||
eventType: SecurityEventType;
|
||||
context: AuditLogContext;
|
||||
metadata: Record<string, any>;
|
||||
metadata: Record<string, unknown>;
|
||||
acknowledged: boolean;
|
||||
acknowledgedBy?: string;
|
||||
acknowledgedAt?: Date;
|
||||
@ -131,7 +131,7 @@ class SecurityMonitoringService {
|
||||
outcome: AuditOutcome,
|
||||
context: AuditLogContext,
|
||||
severity: AuditSeverity = AuditSeverity.INFO,
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
// Add event to buffer for analysis
|
||||
this.eventBuffer.push({
|
||||
@ -377,7 +377,10 @@ class SecurityMonitoringService {
|
||||
/**
|
||||
* Deep merge helper function for config updates
|
||||
*/
|
||||
private deepMerge(target: any, source: any): any {
|
||||
private deepMerge(
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
@ -474,7 +477,7 @@ class SecurityMonitoringService {
|
||||
eventType: SecurityEventType,
|
||||
outcome: AuditOutcome,
|
||||
context: AuditLogContext,
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<Array<Omit<SecurityAlert, "id" | "timestamp" | "acknowledged">>> {
|
||||
const threats: Array<
|
||||
Omit<SecurityAlert, "id" | "timestamp" | "acknowledged">
|
||||
@ -707,12 +710,19 @@ class SecurityMonitoringService {
|
||||
}
|
||||
|
||||
private async calculateUserRiskScores(
|
||||
events: any[]
|
||||
events: Array<{
|
||||
userId?: string;
|
||||
user?: { email: string };
|
||||
eventType: SecurityEventType;
|
||||
outcome: AuditOutcome;
|
||||
severity: AuditSeverity;
|
||||
country?: string;
|
||||
}>
|
||||
): Promise<Array<{ userId: string; email: string; riskScore: number }>> {
|
||||
const userEvents = events.filter((e) => e.userId);
|
||||
const userScores = new Map<
|
||||
string,
|
||||
{ email: string; score: number; events: any[] }
|
||||
{ email: string; score: number; events: typeof events }
|
||||
>();
|
||||
|
||||
for (const event of userEvents) {
|
||||
@ -937,7 +947,7 @@ export async function enhancedSecurityLog(
|
||||
context: AuditLogContext,
|
||||
severity: AuditSeverity = AuditSeverity.INFO,
|
||||
errorMessage?: string,
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
// Log to audit system
|
||||
await securityAuditLogger.log({
|
||||
|
||||
@ -7,6 +7,46 @@ export interface TranscriptFetchResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to prepare request headers
|
||||
*/
|
||||
function prepareRequestHeaders(
|
||||
username?: string,
|
||||
password?: string
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": "LiveDash-Transcript-Fetcher/1.0",
|
||||
};
|
||||
|
||||
if (username && password) {
|
||||
const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
||||
headers.Authorization = authHeader;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to handle network errors
|
||||
*/
|
||||
function handleNetworkError(error: unknown): TranscriptFetchResult {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (errorMessage.includes("ENOTFOUND")) {
|
||||
return { success: false, error: "Domain not found" };
|
||||
}
|
||||
|
||||
if (errorMessage.includes("ECONNREFUSED")) {
|
||||
return { success: false, error: "Connection refused" };
|
||||
}
|
||||
|
||||
if (errorMessage.includes("timeout")) {
|
||||
return { success: false, error: "Request timeout" };
|
||||
}
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch transcript content from a URL
|
||||
* @param url The transcript URL
|
||||
@ -21,29 +61,14 @@ export async function fetchTranscriptContent(
|
||||
): Promise<TranscriptFetchResult> {
|
||||
try {
|
||||
if (!url || !url.trim()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No transcript URL provided",
|
||||
};
|
||||
return { success: false, error: "No transcript URL provided" };
|
||||
}
|
||||
|
||||
// Prepare authentication header if credentials provided
|
||||
const authHeader =
|
||||
username && password
|
||||
? `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
: undefined;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": "LiveDash-Transcript-Fetcher/1.0",
|
||||
};
|
||||
|
||||
if (authHeader) {
|
||||
headers.Authorization = authHeader;
|
||||
}
|
||||
const headers = prepareRequestHeaders(username, password);
|
||||
|
||||
// Fetch the transcript with timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
@ -63,45 +88,12 @@ export async function fetchTranscriptContent(
|
||||
const content = await response.text();
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Empty transcript content",
|
||||
};
|
||||
return { success: false, error: "Empty transcript content" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: content.trim(),
|
||||
};
|
||||
return { success: true, content: content.trim() };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Handle common network errors
|
||||
if (errorMessage.includes("ENOTFOUND")) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Domain not found",
|
||||
};
|
||||
}
|
||||
|
||||
if (errorMessage.includes("ECONNREFUSED")) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Connection refused",
|
||||
};
|
||||
}
|
||||
|
||||
if (errorMessage.includes("timeout")) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Request timeout",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
return handleNetworkError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user