Skip to content
Open
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
44 changes: 39 additions & 5 deletions app/ai-service/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import logging
import uuid
import time
import hmac
import hashlib
import json
from typing import Any, Dict, Optional
from celery import Celery
from celery.result import AsyncResult
Expand Down Expand Up @@ -97,7 +100,15 @@ def update_task_status(
'error': error,
'updated_at': time.time()
}

def sign_payload(payload: dict, secret: str):
timestamp = str(int(time.time()))
payload_str = json.dumps(payload).encode('utf-8')
signature = hmac.new(
secret.encode('utf-8'),
timestamp.encode('utf-8') + b'.' + payload_str,
hashlib.sha256
).hexdigest()
return signature, timestamp

def send_webhook_notification(task_id: str, status: str, result: Any = None, error: str = None) -> None:
"""
Expand Down Expand Up @@ -131,8 +142,33 @@ def send_webhook_notification(task_id: str, status: str, result: Any = None, err
import threading
def send_notification():
try:
# Prepare payload bytes for signing
payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
timestamp_str = str(int(time.time()))

# Get the signing secret
secret = getattr(settings, 'webhook_signing_secret', '')

# Compute signature: sha256 over raw body
# Format required: X-ChainForge-Signature: sha256=<hex>
signature = hmac.new(
secret.encode('utf-8') if secret else b'',
payload_bytes,
hashlib.sha256
).hexdigest()

headers = {
'Content-Type': 'application/json',
'X-ChainForge-Signature': f"sha256={signature}",
'X-ChainForge-Timestamp': timestamp_str
}

with httpx.Client(timeout=10.0) as client:
response = client.post(settings.backend_webhook_url, json=payload)
response = client.post(
settings.backend_webhook_url,
content=payload_bytes,
headers=headers
)
if response.status_code >= 400:
logger.error(f"Webhook notification failed: {response.status_code} - {response.text}")
else:
Expand Down Expand Up @@ -166,8 +202,6 @@ def process_heavy_inference_impl(self, task_id: str, payload: Dict[str, Any]) ->
# Extract task type from payload
task_type = payload.get('type', 'inference')

start_inference = time.time()

# Simulate heavy processing (replace with actual AI inference logic)
# In production, this would handle:
# - Large image processing
Expand Down Expand Up @@ -424,4 +458,4 @@ def create_task(task_type: str, payload: Dict[str, Any]) -> str:

logger.info(f"Created task {task_id} of type {task_type}")

return task_id
return task_id
4 changes: 3 additions & 1 deletion app/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RequestCorrelationMiddleware } from './middleware/request-correlation.m
import { SecurityModule } from './common/security/security.module';
import { CampaignsModule } from './campaigns/campaigns.module';
import { APP_GUARD } from '@nestjs/core';
import { WebhookController } from './webhooks/webhook.controller';
import { ApiKeyGuard } from './common/guards/api-key.guard';
import { RolesGuard } from './auth/roles.guard';
import { ObservabilityModule } from './observability/observability.module';
Expand Down Expand Up @@ -132,7 +133,7 @@ import { SandboxModule } from './sandbox/sandbox.module';
]),
],

controllers: [AppController],
controllers: [AppController, WebhookController],
providers: [
AppService,
{
Expand Down Expand Up @@ -184,3 +185,4 @@ export class AppModule implements NestModule {
);
}
}

6 changes: 4 additions & 2 deletions app/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ async function bootstrap() {
loadEnv({ path: envPath });
}

const app = await NestFactory.create(AppModule);
const app = await NestFactory.create(AppModule, {
rawBody: true,
});

// Get logger instance
const logger = app.get(LoggerService);
Expand Down Expand Up @@ -103,5 +105,5 @@ async function bootstrap() {
logger.log(`📚 API Documentation: http://localhost:${port}/api/docs`);
logger.log(`🔍 API Version: v1`);
}

void bootstrap();
25 changes: 25 additions & 0 deletions app/backend/src/webhooks/webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus, Logger } from '@nestjs/common';
import { WebhookGuard } from './webhook.guard';

@Controller('webhooks')
export class WebhookController {
private readonly logger = new Logger(WebhookController.name);

@Post()
@UseGuards(WebhookGuard)
@HttpCode(HttpStatus.OK)
async handleWebhook(@Body() payload: any) {
this.logger.log('Received verified webhook payload successfully');

// Here we will handle the incoming task results
// Example: Update the claim status in the database based on the AI service task output
const { taskId, status, result } = payload;

this.logger.log(`Task ${taskId} completed with status: ${status}`);

Check failure on line 19 in app/backend/src/webhooks/webhook.controller.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Async method 'handleWebhook' has no 'await' expression
// TODO: Integrate with your Claims/Campaigns service to update state

return { received: true };
}
}

Check failure on line 24 in app/backend/src/webhooks/webhook.controller.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'result' is assigned a value but never used. Allowed unused vars must match /^_/u

69 changes: 69 additions & 0 deletions app/backend/src/webhooks/webhook.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import * as crypto from 'crypto';

@Injectable()
export class WebhookGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {

Check failure on line 11 in app/backend/src/webhooks/webhook.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Async method 'canActivate' has no 'await' expression
const request = context.switchToHttp().getRequest();

// 1. Extract headers
const signatureHeader = request.headers['x-chainforge-signature'] as string;
const timestampHeader = request.headers['x-chainforge-timestamp'] as string;

if (!signatureHeader || !timestampHeader) {
throw new UnauthorizedException('Missing signature or timestamp headers');
}

// 2. Replay attack prevention: Ensure the timestamp is within a 5-minute window (300 seconds)
const currentTime = Math.floor(Date.now() / 1000);
const requestTime = parseInt(timestampHeader, 10);

if (isNaN(requestTime) || Math.abs(currentTime - requestTime) > 300) {
throw new UnauthorizedException('Request timestamp expired or invalid');
}

// 3. Get signature secret
const secret = process.env.WEBHOOK_SIGNING_SECRET;
if (!secret) {
throw new UnauthorizedException('Webhook signing secret is not configured');
}

// 4. Retrieve raw body (NestJS raw body is required for verification)
const rawBody = request.rawBody;
if (!rawBody) {
throw new UnauthorizedException('Raw request body is missing for signature verification');
}

// Extract signature hash from format: sha256=<hex_signature>
const expectedSignature = signatureHeader.startsWith('sha256=')
? signatureHeader.slice(7)
: signatureHeader;

// 5. Compute expected HMAC
const hmac = crypto.createHmac('sha256', secret);
hmac.update(timestampHeader + '.' + rawBody.toString());
const computedSignature = hmac.digest('hex');

// 6. Prevent timing attacks using timingSafeEqual
try {
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
const computedBuffer = Buffer.from(computedSignature, 'hex');

if (
expectedBuffer.length !== computedBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, computedBuffer)
) {
throw new UnauthorizedException('Invalid webhook signature');
}
} catch (error) {
throw new UnauthorizedException('Invalid signature format');
}

return true;

Check failure on line 67 in app/backend/src/webhooks/webhook.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'error' is defined but never used
}
}
6 changes: 6 additions & 0 deletions docs/webhooks/signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Webhook Signing

All webhooks are signed using HMAC SHA256.
The secret key is stored in the environment variable `WEBHOOK_SIGNING_SECRET`.

To verify the signature, calculate the HMAC SHA256 of the raw request body using your secret key.
100 changes: 100 additions & 0 deletions src/prisma/docs/observability/prisma-slow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"title": "Prisma Query Duration Percentiles (p50 / p95 / p99)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "histogram_quantile(0.50, sum(rate(prisma_query_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p50 - Median",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(prisma_query_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p95 - Slow Queries",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(prisma_query_duration_seconds_bucket[5m])) by (le))",
"legendFormat": "p99 - Slowest Queries",
"range": true,
"refId": "C"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth"
},
"unit": "s",
"displayName": "${__series.name}"
}
}
}
],
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Prisma Query Performance",
"version": 1
}
29 changes: 29 additions & 0 deletions src/prisma/prisma-metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Prisma } from '@prisma/client';
import client from 'prom-client';

// Create the histogram metric for Prometheus
const prismaQueryDuration = new client.Histogram({
name: 'prisma_query_duration_seconds',
help: 'Duration of Prisma queries in seconds',
labelNames: ['model', 'action'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
});

export const prismaMetricsExtension = Prisma.defineExtension((prismaClient) => {
return prismaClient.$extends({
query: {
$allOperations({ model, operation, args, query }) {
const startTime = process.hrtime();

return query(args).finally(() => {
const [seconds, nanoseconds] = process.hrtime(startTime);
const durationInSeconds = seconds + nanoseconds / 1e9;

if (model) {
prismaQueryDuration.labels({ model, action: operation }).observe(durationInSeconds);
}
});
},
},
});
});
Loading