- Deterministic Classification: MOAS/HIJACK/LEAK type detection - Severity scoring: CRITICAL/HIGH/MEDIUM/LOW based on prefix length - Optional Ollama enrichment (qwen2.5:3b) for CRITICAL only (5s timeout) - PostgreSQL backend: hijack_events, webhook_subscriptions, webhook_deliveries - HMAC-SHA256 webhook signing with exponential backoff retry - Retry scheduler: node-cron job every 5 minutes - 6 API endpoints: POST/GET/DELETE webhooks, test delivery, list/resolve hijacks - 22 comprehensive tests (80%+ coverage) - Zero external API costs (deterministic + local Ollama only)
55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
import axios, { AxiosError } from 'axios'
|
|
import crypto from 'crypto'
|
|
import type { WebhookPayload, HijackEvent } from './types'
|
|
|
|
export class WebhookClient {
|
|
async sendWebhook(event: HijackEvent, endpoint_url: string, secret_key: string, timeout_ms: number = 10000): Promise<{ success: boolean; status?: number; error?: string; response_time_ms: number }> {
|
|
const startTime = Date.now()
|
|
const timestamp = new Date().toISOString()
|
|
|
|
const payload = {
|
|
event,
|
|
timestamp
|
|
}
|
|
|
|
const signature = this.generateSignature(JSON.stringify(payload), secret_key)
|
|
|
|
try {
|
|
const response = await axios.post(endpoint_url, payload, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Webhook-Signature': signature,
|
|
'X-Webhook-Timestamp': timestamp
|
|
},
|
|
timeout: timeout_ms
|
|
})
|
|
|
|
return {
|
|
success: response.status >= 200 && response.status < 300,
|
|
status: response.status,
|
|
response_time_ms: Date.now() - startTime
|
|
}
|
|
} catch (error) {
|
|
const axiosError = error as AxiosError
|
|
return {
|
|
success: false,
|
|
status: axiosError.response?.status,
|
|
error: axiosError.message,
|
|
response_time_ms: Date.now() - startTime
|
|
}
|
|
}
|
|
}
|
|
|
|
calculateBackoffDelay(attemptNumber: number): number {
|
|
if (attemptNumber === 0) return 0.5
|
|
const baseDelay = 1000
|
|
const delayMs = baseDelay * Math.pow(2, attemptNumber - 1)
|
|
const maxDelay = 32000
|
|
return Math.min(delayMs, maxDelay)
|
|
}
|
|
|
|
private generateSignature(payload: string, secret: string): string {
|
|
return crypto.createHmac('sha256', secret).update(payload).digest('hex')
|
|
}
|
|
}
|