From 4f01551b5226276cd1d1adf434eea3f15c9329a2 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:45:44 +0100 Subject: [PATCH 01/13] Create prisma-metrics.ts --- src/prisma/prisma-metrics.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/prisma/prisma-metrics.ts diff --git a/src/prisma/prisma-metrics.ts b/src/prisma/prisma-metrics.ts new file mode 100644 index 00000000..f0daabf2 --- /dev/null +++ b/src/prisma/prisma-metrics.ts @@ -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); + } + }); + }, + }, + }); +}); From 993f32aa6022643b9837381654d61e6487c37dfa Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:48:16 +0100 Subject: [PATCH 02/13] Create prisma-slow.json --- .../docs/observability/prisma-slow.json | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/prisma/docs/observability/prisma-slow.json diff --git a/src/prisma/docs/observability/prisma-slow.json b/src/prisma/docs/observability/prisma-slow.json new file mode 100644 index 00000000..6b1328fd --- /dev/null +++ b/src/prisma/docs/observability/prisma-slow.json @@ -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 +} From ce5d12e5696c2912bf5e610498acfca898aacdbc Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:32:17 +0100 Subject: [PATCH 03/13] feat(ai-service): add webhook hmac signature and timestamp signing --- app/ai-service/tasks.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/app/ai-service/tasks.py b/app/ai-service/tasks.py index 53f21fb4..b568232b 100644 --- a/app/ai-service/tasks.py +++ b/app/ai-service/tasks.py @@ -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 @@ -131,8 +134,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= + 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: @@ -166,8 +194,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 @@ -424,4 +450,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 \ No newline at end of file + return task_id From 0437f2ba14d3d18e30724650a3e45884f44b063d Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:47:12 +0100 Subject: [PATCH 04/13] feat(backend): add WebhookGuard for signature and timestamp verification --- app/backend/src/webhooks/webhook.guard.ts | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/backend/src/webhooks/webhook.guard.ts diff --git a/app/backend/src/webhooks/webhook.guard.ts b/app/backend/src/webhooks/webhook.guard.ts new file mode 100644 index 00000000..0c9a134a --- /dev/null +++ b/app/backend/src/webhooks/webhook.guard.ts @@ -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 { + 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= + const expectedSignature = signatureHeader.startsWith('sha256=') + ? signatureHeader.slice(7) + : signatureHeader; + + // 5. Compute expected HMAC + const hmac = crypto.createHmac('sha256', secret); + hmac.update(rawBody); + 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; + } +} From 2fdd1d95c15585c6d81ad2754184937eece4327d Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:48:12 +0100 Subject: [PATCH 05/13] feat(backend): add WebhookController with WebhookGuard protection --- .../src/webhooks/webhook.controller.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/backend/src/webhooks/webhook.controller.ts diff --git a/app/backend/src/webhooks/webhook.controller.ts b/app/backend/src/webhooks/webhook.controller.ts new file mode 100644 index 00000000..6f1b7ba6 --- /dev/null +++ b/app/backend/src/webhooks/webhook.controller.ts @@ -0,0 +1,24 @@ +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}`); + + // TODO: Integrate with your Claims/Campaigns service to update state + + return { received: true }; + } +} From e61c68a02708bdee32cc95a5dc44d6006f660b15 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:57:07 +0100 Subject: [PATCH 06/13] feat(backend): register WebhookController in AppModule --- app/backend/src/app.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 3935b65e..57f04cdf 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -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'; @@ -132,7 +133,7 @@ import { SandboxModule } from './sandbox/sandbox.module'; ]), ], - controllers: [AppController], + controllers: [AppController, WebhookController], providers: [ AppService, { From b8a4452fd78e16c63b58b9b410603445c68a1c08 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:00:43 +0100 Subject: [PATCH 07/13] feat(backend): enable rawBody in main.ts for webhook security --- app/backend/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/backend/src/main.ts b/app/backend/src/main.ts index 9fa25572..2b751902 100644 --- a/app/backend/src/main.ts +++ b/app/backend/src/main.ts @@ -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); From 70f89cfeaa1dd80f65c09374c5ee1941d50c7b69 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:03:08 +0100 Subject: [PATCH 08/13] docs: add webhook signing documentation --- docs/webhooks/signing.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/webhooks/signing.md diff --git a/docs/webhooks/signing.md b/docs/webhooks/signing.md new file mode 100644 index 00000000..63fcce64 --- /dev/null +++ b/docs/webhooks/signing.md @@ -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. From 1cda269a4b400d4502ccf2ee2e0197449f4a1498 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:24:35 +0100 Subject: [PATCH 09/13] feat(ai-service): add webhook hmac signature and timestamp signing Implements HMAC SHA256 signature generation for webhook notifications to ensure data integrity. --- app/ai-service/tasks.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/ai-service/tasks.py b/app/ai-service/tasks.py index b568232b..4d07b065 100644 --- a/app/ai-service/tasks.py +++ b/app/ai-service/tasks.py @@ -100,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: """ From 9692c1def25ea71a958b1d125c30cbe8eafa7005 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:35:08 +0100 Subject: [PATCH 10/13] fix(backend): include timestamp in HMAC verification --- app/backend/src/webhooks/webhook.guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/backend/src/webhooks/webhook.guard.ts b/app/backend/src/webhooks/webhook.guard.ts index 0c9a134a..a142db4c 100644 --- a/app/backend/src/webhooks/webhook.guard.ts +++ b/app/backend/src/webhooks/webhook.guard.ts @@ -46,7 +46,7 @@ export class WebhookGuard implements CanActivate { // 5. Compute expected HMAC const hmac = crypto.createHmac('sha256', secret); - hmac.update(rawBody); + hmac.update(timestampHeader + '.' + rawBody.toString()); const computedSignature = hmac.digest('hex'); // 6. Prevent timing attacks using timingSafeEqual From 1b2abb036593572b2ce7a6722e6af7da191f4730 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:40:35 +0100 Subject: [PATCH 11/13] feat(backend): implement WebhookController with WebhookGuard protection (Issue #221) --- app/backend/src/webhooks/webhook.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/backend/src/webhooks/webhook.controller.ts b/app/backend/src/webhooks/webhook.controller.ts index 6f1b7ba6..c10ab095 100644 --- a/app/backend/src/webhooks/webhook.controller.ts +++ b/app/backend/src/webhooks/webhook.controller.ts @@ -22,3 +22,4 @@ export class WebhookController { return { received: true }; } } + From 6fdc4cb99a256e0c02f77c9214b2f4ebe110eb81 Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:44:31 +0100 Subject: [PATCH 12/13] feat(backend): register WebhookController in AppModule (Issue #221) --- app/backend/src/app.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 57f04cdf..b95d32b7 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -185,3 +185,4 @@ export class AppModule implements NestModule { ); } } + From f1899684dc25d3367121ce659118d970d7df799b Mon Sep 17 00:00:00 2001 From: birdcoin0 <122475622+birdcoin0@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:46:47 +0100 Subject: [PATCH 13/13] feat(backend): enable rawBody in main.ts for webhook security (Issue #221) --- app/backend/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/backend/src/main.ts b/app/backend/src/main.ts index 2b751902..f1c4ee2e 100644 --- a/app/backend/src/main.ts +++ b/app/backend/src/main.ts @@ -105,5 +105,5 @@ async function bootstrap() { logger.log(`📚 API Documentation: http://localhost:${port}/api/docs`); logger.log(`🔍 API Version: v1`); } - + void bootstrap();