Skip to content

Commit 43652f6

Browse files
Mainnet-ops“Mainnet-ops”
andauthored
docs: add SSE and webhook event documentation to Swagger (#431)
Add comprehensive OpenAPI documentation for the real-time event systems: - Create PolicyStatusEventDto for SSE event payload schema - Create webhook DTOs (RegisterWebhookDto, WebhookRegistrationResponseDto, WebhookListItemDto, PolicyStatusChangePayloadDto, ClaimStatusChangePayloadDto) with validation decorators and Swagger metadata - Document SSE endpoint with event data schema, status transition table, and EventSource connection example - Document webhook registration with event type table, payload schemas, and HMAC-SHA256 signature verification instructions - Add 'webhooks' and 'events' tags to Swagger config in DocumentBuilder Co-authored-by: “Mainnet-ops” <“footballwatch68@gmail.com”>
1 parent c9488d4 commit 43652f6

5 files changed

Lines changed: 166 additions & 9 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class PolicyStatusEventDto {
4+
@ApiProperty({ description: 'Policy UUID', example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' })
5+
policyId: string;
6+
7+
@ApiProperty({
8+
description: 'Current policy status',
9+
example: 'ACTIVE',
10+
enum: ['ACTIVE', 'EXPIRED', 'CANCELLED', 'CLAIMED', 'PROCESSING'],
11+
})
12+
status: string;
13+
14+
@ApiProperty({ description: 'Unix timestamp in milliseconds', example: 1700000000000 })
15+
timestamp: number;
16+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsUrl, IsArray, ArrayNotEmpty, ArrayUnique, IsOptional, IsString, IsEnum } from 'class-validator';
3+
4+
export enum WebhookEventType {
5+
POLICY_STATUS_CHANGE = 'policy.status.change',
6+
CLAIM_STATUS_CHANGE = 'claim.status.change',
7+
}
8+
9+
export class RegisterWebhookDto {
10+
@ApiProperty({ description: 'URL to receive webhook POST requests', example: 'https://example.com/webhook' })
11+
@IsUrl()
12+
url: string;
13+
14+
@ApiProperty({
15+
description: 'Event types to subscribe to',
16+
enum: WebhookEventType,
17+
isArray: true,
18+
example: [WebhookEventType.POLICY_STATUS_CHANGE, WebhookEventType.CLAIM_STATUS_CHANGE],
19+
})
20+
@IsArray()
21+
@ArrayNotEmpty()
22+
@ArrayUnique()
23+
@IsEnum(WebhookEventType, { each: true })
24+
events: WebhookEventType[];
25+
26+
@ApiPropertyOptional({
27+
description: 'Shared secret for HMAC-SHA256 signature verification. If provided, each delivery includes an X-Webhook-Signature header.',
28+
example: 'whsec_abc123',
29+
})
30+
@IsOptional()
31+
@IsString()
32+
secret?: string;
33+
}
34+
35+
export class WebhookRegistrationResponseDto {
36+
@ApiProperty({ description: 'Unique webhook registration ID', example: '1700000000000-abc1234' })
37+
id: string;
38+
39+
@ApiProperty({ description: 'Registration status', example: 'registered' })
40+
status: string;
41+
}
42+
43+
export class WebhookListItemDto {
44+
@ApiProperty({ description: 'Unique webhook registration ID', example: '1700000000000-abc1234' })
45+
id: string;
46+
47+
@ApiProperty({ description: 'Target URL for deliveries', example: 'https://example.com/webhook' })
48+
url: string;
49+
50+
@ApiProperty({ description: 'Subscribed event types', enum: WebhookEventType, isArray: true })
51+
events: WebhookEventType[];
52+
53+
@ApiProperty({ description: 'Whether the webhook is active', example: true })
54+
isActive: boolean;
55+
56+
@ApiProperty({ description: 'Registration timestamp' })
57+
createdAt: Date;
58+
}
59+
60+
export class PolicyStatusChangePayloadDto {
61+
@ApiProperty({ description: 'Policy UUID', example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' })
62+
policyId: string;
63+
64+
@ApiProperty({ description: 'Previous policy status', example: 'ACTIVE', enum: ['ACTIVE', 'EXPIRED', 'CANCELLED', 'CLAIMED', 'PROCESSING'] })
65+
fromStatus: string;
66+
67+
@ApiProperty({ description: 'New policy status', example: 'CLAIMED', enum: ['ACTIVE', 'EXPIRED', 'CANCELLED', 'CLAIMED', 'PROCESSING'] })
68+
toStatus: string;
69+
70+
@ApiProperty({ description: 'Unix timestamp in milliseconds', example: 1700000000000 })
71+
timestamp: number;
72+
}
73+
74+
export class ClaimStatusChangePayloadDto {
75+
@ApiProperty({ description: 'Claim UUID', example: 'b2c3d4e5-f6a7-8901-bcde-f12345678901' })
76+
claimId: string;
77+
78+
@ApiProperty({ description: 'Previous claim status', example: 'PROCESSING' })
79+
fromStatus: string;
80+
81+
@ApiProperty({ description: 'New claim status', example: 'PAID' })
82+
toStatus: string;
83+
84+
@ApiProperty({ description: 'Unix timestamp in milliseconds', example: 1700000000000 })
85+
timestamp: number;
86+
}

src/common/webhooks/webhooks.controller.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
2-
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
1+
import { Controller, Post, Body, Get } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
33
import { WebhooksService } from '../events/webhooks.service';
44
import { PrismaService } from '../prisma/prisma.service';
5+
import {
6+
RegisterWebhookDto,
7+
WebhookRegistrationResponseDto,
8+
WebhookListItemDto,
9+
PolicyStatusChangePayloadDto,
10+
ClaimStatusChangePayloadDto,
11+
} from './dto/webhook.dto';
512

613
@Controller('webhooks')
714
@ApiTags('webhooks')
15+
@ApiExtraModels(RegisterWebhookDto, WebhookRegistrationResponseDto, WebhookListItemDto, PolicyStatusChangePayloadDto, ClaimStatusChangePayloadDto)
816
export class WebhooksController {
917
constructor(
1018
private readonly webhooks: WebhooksService,
@@ -13,14 +21,32 @@ export class WebhooksController {
1321

1422
/** POST /api/v1/webhooks/register — register a webhook endpoint */
1523
@Post('register')
16-
@ApiOperation({ summary: 'Register a webhook for policy/claim status changes' })
24+
@ApiOperation({
25+
summary: 'Register a webhook for real-time event notifications',
26+
description:
27+
'Register a URL to receive POST requests when specific events occur. ' +
28+
'Supported event types:\n\n' +
29+
'| Event | Description | Payload |\n' +
30+
'|-------|-------------|---------|\n' +
31+
'| `policy.status.change` | A policy status transition (e.g. ACTIVE → CLAIMED) | `{ policyId, fromStatus, toStatus, timestamp }` |\n' +
32+
'| `claim.status.change` | A claim status transition (e.g. PROCESSING → PAID) | `{ claimId, fromStatus, toStatus, timestamp }` |\n\n' +
33+
'**Signature verification:** If a `secret` is provided, each delivery includes an `X-Webhook-Signature` header ' +
34+
'containing an HMAC-SHA256 digest of the JSON payload, base64-encoded. Verify with:\n' +
35+
'```\n' +
36+
'crypto.createHmac("sha256", secret).update(rawBody).digest("base64")\n' +
37+
'```',
38+
})
1739
@ApiBearerAuth()
18-
@ApiResponse({ status: 201, description: 'Webhook registered successfully' })
40+
@ApiResponse({
41+
status: 201,
42+
description: 'Webhook registered successfully',
43+
schema: { $ref: getSchemaPath(WebhookRegistrationResponseDto) },
44+
})
1945
@ApiResponse({ status: 400, description: 'Invalid request body' })
20-
register(@Body() dto: { url: string; events: string[]; secret?: string }) {
46+
register(@Body() dto: RegisterWebhookDto) {
2147
return this.webhooks.registerWebhook({
2248
url: dto.url,
23-
events: dto.events as ('policy.status.change' | 'claim.status.change')[],
49+
events: dto.events,
2450
secret: dto.secret,
2551
});
2652
}
@@ -29,7 +55,11 @@ export class WebhooksController {
2955
@Get()
3056
@ApiOperation({ summary: 'List all registered webhooks' })
3157
@ApiBearerAuth()
32-
@ApiResponse({ status: 200, description: 'Returns list of registered webhooks' })
58+
@ApiResponse({
59+
status: 200,
60+
description: 'Returns list of active webhook registrations',
61+
schema: { type: 'array', items: { $ref: getSchemaPath(WebhookListItemDto) } },
62+
})
3363
list() {
3464
return this.webhooks.getRegistrations();
3565
}

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ async function bootstrap() {
138138
.addTag('oracle', 'Oracle data feeds and readings')
139139
.addTag('auth', 'Wallet-based authentication')
140140
.addTag('health', 'Service health monitoring')
141+
.addTag('webhooks', 'Webhook registration and real-time event subscriptions')
142+
.addTag('events', 'Server-Sent Events (SSE) for real-time policy status streaming')
141143
.build();
142144
const document = SwaggerModule.createDocument(app, swaggerConfig);
143145
SwaggerModule.setup('docs', app, document);

src/policy/policy.controller.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
3939
import { OperatorAuthGuard } from '../auth/operator-auth.guard';
4040
import { AuthenticatedRequest } from '../auth/authenticated-request';
4141
import { StatusEventsService } from '../common/events/status-events.service';
42+
import { PolicyStatusEventDto } from '../common/events/dto/sse-event.dto';
4243

4344
@ApiTags('policy')
4445
@Controller()
45-
@ApiExtraModels(ResponseDto, PaginatedResponseDto, ProductResponseDto, PolicyResponseDto)
46+
@ApiExtraModels(ResponseDto, PaginatedResponseDto, ProductResponseDto, PolicyResponseDto, PolicyStatusEventDto)
4647
export class PolicyController {
4748
constructor(
4849
private readonly policy: PolicyService,
@@ -376,8 +377,30 @@ export class PolicyController {
376377
@Sse('policies/:id/events')
377378
@UseGuards(JwtAuthGuard)
378379
@ApiBearerAuth()
379-
@ApiOperation({ summary: 'Server-Sent Events stream of status changes for a policy' })
380+
@ApiOperation({
381+
summary: 'Server-Sent Events stream of status changes for a policy',
382+
description:
383+
'Opens an SSE connection that streams policy status transitions in real time. ' +
384+
'The current status is emitted immediately on connection, followed by events whenever the status changes. ' +
385+
'Possible status values: ACTIVE, PROCESSING, CLAIMED, CANCELLED, EXPIRED.\n\n' +
386+
'Event data schema:\n' +
387+
'```json\n' +
388+
'{ "policyId": "uuid", "status": "ACTIVE", "timestamp": 1700000000000 }\n' +
389+
'```\n\n' +
390+
'Connect with `EventSource`:\n' +
391+
'```js\n' +
392+
'const es = new EventSource("/api/v1/policies/:id/events", { withCredentials: true });\n' +
393+
"es.onmessage = (e) => console.log(JSON.parse(e.data));\n" +
394+
'```',
395+
})
380396
@ApiParam({ name: 'id', description: 'Policy UUID' })
397+
@ApiResponse({
398+
status: 200,
399+
description: 'SSE stream of PolicyStatusEvent objects. Each message has a `data` field containing the event payload.',
400+
schema: { $ref: getSchemaPath(PolicyStatusEventDto) },
401+
})
402+
@ApiResponse({ status: 403, description: 'Policy belongs to a different wallet' })
403+
@ApiResponse({ status: 404, description: 'Policy not found' })
381404
async policyStatusEvents(
382405
@Param('id') id: string,
383406
@Req() req: AuthenticatedRequest,

0 commit comments

Comments
 (0)