Enhance mapping and orchestrator services with new features and improvements
- Updated `mapping-service` to include WEB3-ETH-IBAN support, health check endpoint, and improved error handling for account-wallet linking. - Added new provider connection and status endpoints in `mapping-service`. - Enhanced `orchestrator` service with health check, trigger management endpoints, and improved error handling for trigger validation and submission. - Updated dependencies in `package.json` for both services, including `axios`, `uuid`, and type definitions. - Improved packet service with additional validation and error handling for packet generation and dispatching. - Introduced webhook service enhancements, including delivery retries, dead letter queue management, and webhook management endpoints.
This commit is contained in:
@@ -5,8 +5,7 @@
|
||||
|
||||
import express from 'express';
|
||||
import { webhookRouter } from './routes/webhooks';
|
||||
import { eventBusClient } from '@emoney/events';
|
||||
import { webhookDeliveryService } from './services/delivery';
|
||||
import { initializeEventBusSubscriptions } from './services/event-bus';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -16,9 +15,12 @@ app.use(express.json());
|
||||
// Webhook management API
|
||||
app.use('/v1/webhooks', webhookRouter);
|
||||
|
||||
// Subscribe to event bus and deliver webhooks
|
||||
eventBusClient.on('published', async ({ topic, event }) => {
|
||||
await webhookDeliveryService.deliverToSubscribers(topic, event);
|
||||
// Initialize event bus subscriptions
|
||||
initializeEventBusSubscriptions();
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', service: 'webhook-service' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@@ -48,6 +48,52 @@ webhookRouter.post('/:id/replay', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete webhook
|
||||
webhookRouter.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await webhookService.deleteWebhook(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error: any) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get delivery attempts
|
||||
webhookRouter.get('/:id/attempts', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { storage } = await import('../services/storage');
|
||||
const attempts = await storage.getDeliveryAttempts(req.params.id);
|
||||
res.json({ attempts });
|
||||
} catch (error: any) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DLQ endpoints
|
||||
webhookRouter.get('/dlq', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { storage } = await import('../services/storage');
|
||||
const { limit, offset } = req.query;
|
||||
const result = await storage.getDLQEntries(
|
||||
limit ? parseInt(limit as string) : 20,
|
||||
offset ? parseInt(offset as string) : 0
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
webhookRouter.post('/dlq/:id/retry', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { webhookDeliveryService } = await import('../services/delivery');
|
||||
await webhookDeliveryService.retryDLQEntry(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get webhook
|
||||
webhookRouter.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
* Webhook delivery service with retry logic and DLQ
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import crypto from 'crypto';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { createHmac } from 'crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { storage, DeliveryAttempt, DLQEntry } from './storage';
|
||||
|
||||
export interface DeliveryAttempt {
|
||||
webhookId: string;
|
||||
@@ -15,12 +17,81 @@ export interface DeliveryAttempt {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY_BASE = 1000; // 1 second base delay
|
||||
|
||||
export const webhookDeliveryService = {
|
||||
/**
|
||||
* Deliver event to all webhooks subscribed to topic
|
||||
*/
|
||||
async deliverToSubscribers(topic: string, event: any): Promise<void> {
|
||||
// TODO: Get all webhooks subscribed to this topic
|
||||
// TODO: For each webhook, deliver with retry logic
|
||||
// Get all webhooks subscribed to this topic/event
|
||||
const eventType = event.eventType || topic;
|
||||
const webhooks = await storage.listWebhooks({
|
||||
enabled: true,
|
||||
event: eventType
|
||||
});
|
||||
|
||||
// Deliver to each webhook
|
||||
const deliveries = webhooks.map(webhook =>
|
||||
this.deliverWithRetry(webhook.id, webhook.url, event, webhook.secret)
|
||||
.catch(error => {
|
||||
console.error(`Failed to deliver webhook ${webhook.id}:`, error);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(deliveries);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deliver webhook with retry logic
|
||||
*/
|
||||
async deliverWithRetry(
|
||||
webhookId: string,
|
||||
url: string,
|
||||
event: any,
|
||||
secret?: string,
|
||||
maxRetries: number = MAX_RETRIES
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
await this.deliver(webhookId, url, event, secret);
|
||||
return; // Success
|
||||
} catch (error: any) {
|
||||
const isLastAttempt = attempt === maxRetries;
|
||||
|
||||
// Record delivery attempt
|
||||
const attemptRecord: DeliveryAttempt = {
|
||||
id: uuidv4(),
|
||||
webhookId,
|
||||
url,
|
||||
event,
|
||||
attempt,
|
||||
status: isLastAttempt ? 'failed' : 'pending',
|
||||
error: error.message,
|
||||
timestamp: Date.now(),
|
||||
responseCode: (error as AxiosError).response?.status,
|
||||
responseBody: (error as AxiosError).response?.data as string,
|
||||
};
|
||||
|
||||
await storage.saveDeliveryAttempt(attemptRecord);
|
||||
|
||||
if (isLastAttempt) {
|
||||
// Move to dead letter queue
|
||||
await this.moveToDLQ(webhookId, url, event, attempt, error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, attempt - 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deliver webhook (single attempt)
|
||||
*/
|
||||
async deliver(webhookId: string, url: string, event: any, secret?: string): Promise<void> {
|
||||
const payload = JSON.stringify(event);
|
||||
const signature = secret ? this.signPayload(payload, secret) : undefined;
|
||||
@@ -28,6 +99,8 @@ export const webhookDeliveryService = {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'eMoney-Webhook/1.0',
|
||||
'X-Webhook-Id': webhookId,
|
||||
'X-Event-Type': event.eventType || 'unknown',
|
||||
};
|
||||
|
||||
if (signature) {
|
||||
@@ -35,43 +108,107 @@ export const webhookDeliveryService = {
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
const response = await axios.post(url, payload, {
|
||||
headers,
|
||||
timeout: 10000,
|
||||
validateStatus: (status) => status >= 200 && status < 300,
|
||||
});
|
||||
|
||||
// Record successful delivery
|
||||
const attemptRecord: DeliveryAttempt = {
|
||||
id: uuidv4(),
|
||||
webhookId,
|
||||
url,
|
||||
event,
|
||||
attempt: 1,
|
||||
status: 'success',
|
||||
timestamp: Date.now(),
|
||||
responseCode: response.status,
|
||||
responseBody: JSON.stringify(response.data),
|
||||
};
|
||||
|
||||
await storage.saveDeliveryAttempt(attemptRecord);
|
||||
} catch (error: any) {
|
||||
// TODO: Retry with exponential backoff
|
||||
// TODO: Move to DLQ after max retries
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError;
|
||||
throw new Error(
|
||||
`Webhook delivery failed: ${axiosError.response?.status} ${axiosError.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign payload with HMAC-SHA256
|
||||
*/
|
||||
signPayload(payload: string, secret: string): string {
|
||||
return crypto
|
||||
.createHmac('sha256', secret)
|
||||
return createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
},
|
||||
|
||||
async retryWithBackoff(
|
||||
/**
|
||||
* Move failed delivery to dead letter queue
|
||||
*/
|
||||
async moveToDLQ(
|
||||
webhookId: string,
|
||||
url: string,
|
||||
event: any,
|
||||
maxRetries: number = 3
|
||||
attempts: number,
|
||||
lastError?: string
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
await this.deliver(webhookId, url, event);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt === maxRetries) {
|
||||
// TODO: Move to dead letter queue
|
||||
throw error;
|
||||
}
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));
|
||||
}
|
||||
const dlqEntry: DLQEntry = {
|
||||
id: uuidv4(),
|
||||
webhookId,
|
||||
url,
|
||||
event,
|
||||
attempts,
|
||||
lastError,
|
||||
createdAt: Date.now(),
|
||||
failedAt: Date.now(),
|
||||
};
|
||||
|
||||
await storage.saveToDLQ(dlqEntry);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry DLQ entry
|
||||
*/
|
||||
async retryDLQEntry(dlqId: string): Promise<void> {
|
||||
const entries = await storage.getDLQEntries(1000, 0);
|
||||
const entry = entries.entries.find(e => e.id === dlqId);
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(`DLQ entry not found: ${dlqId}`);
|
||||
}
|
||||
|
||||
// Get webhook to retrieve secret
|
||||
const webhook = await storage.getWebhook(entry.webhookId);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${entry.webhookId}`);
|
||||
}
|
||||
|
||||
// Retry delivery
|
||||
try {
|
||||
await this.deliverWithRetry(
|
||||
entry.webhookId,
|
||||
entry.url,
|
||||
entry.event,
|
||||
webhook.secret
|
||||
);
|
||||
|
||||
// Remove from DLQ on success
|
||||
await storage.removeFromDLQ(dlqId);
|
||||
} catch (error) {
|
||||
// Update DLQ entry with new error
|
||||
await storage.saveToDLQ({
|
||||
...entry,
|
||||
attempts: entry.attempts + 1,
|
||||
lastError: (error as Error).message,
|
||||
failedAt: Date.now(),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
47
api/services/webhook-service/src/services/event-bus.ts
Normal file
47
api/services/webhook-service/src/services/event-bus.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Event bus integration for webhook service
|
||||
*/
|
||||
|
||||
import { eventBusClient } from '@emoney/events';
|
||||
import { webhookDeliveryService } from './delivery';
|
||||
|
||||
/**
|
||||
* Subscribe to event bus and trigger webhook delivery
|
||||
*/
|
||||
export function initializeEventBusSubscriptions() {
|
||||
// Subscribe to all event types
|
||||
const eventTypes = [
|
||||
'triggers.created',
|
||||
'triggers.state.updated',
|
||||
'liens.placed',
|
||||
'liens.reduced',
|
||||
'liens.released',
|
||||
'packets.generated',
|
||||
'packets.dispatched',
|
||||
'packets.acknowledged',
|
||||
'bridge.locked',
|
||||
'bridge.unlocked',
|
||||
'compliance.updated',
|
||||
'policy.updated',
|
||||
'mappings.created',
|
||||
'mappings.deleted',
|
||||
];
|
||||
|
||||
// Listen for published events
|
||||
eventBusClient.on('published', async ({ topic, event }) => {
|
||||
try {
|
||||
await webhookDeliveryService.deliverToSubscribers(topic, event);
|
||||
} catch (error) {
|
||||
console.error(`Failed to deliver webhook for topic ${topic}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Also subscribe to specific topics if event bus supports it
|
||||
eventTypes.forEach(eventType => {
|
||||
// In production, subscribe to each topic
|
||||
// eventBusClient.subscribe(eventType, async (event) => {
|
||||
// await webhookDeliveryService.deliverToSubscribers(eventType, event);
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
30
api/services/webhook-service/src/services/http-client.ts
Normal file
30
api/services/webhook-service/src/services/http-client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* HTTP client for webhook delivery
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
class HttpClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'eMoney-Webhook/1.0',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver webhook payload
|
||||
*/
|
||||
async deliver(url: string, payload: any, headers: Record<string, string>): Promise<any> {
|
||||
const response = await this.client.post(url, payload, { headers });
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const httpClient = new HttpClient();
|
||||
|
||||
148
api/services/webhook-service/src/services/storage.ts
Normal file
148
api/services/webhook-service/src/services/storage.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Storage layer for webhooks
|
||||
* In-memory implementation with database-ready interface
|
||||
*/
|
||||
|
||||
export interface Webhook {
|
||||
id: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
secret?: string;
|
||||
enabled: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface DeliveryAttempt {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
url: string;
|
||||
event: any;
|
||||
attempt: number;
|
||||
status: 'pending' | 'success' | 'failed';
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
responseCode?: number;
|
||||
responseBody?: string;
|
||||
}
|
||||
|
||||
export interface DLQEntry {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
url: string;
|
||||
event: any;
|
||||
attempts: number;
|
||||
lastError?: string;
|
||||
createdAt: number;
|
||||
failedAt: number;
|
||||
}
|
||||
|
||||
export interface StorageAdapter {
|
||||
// Webhook operations
|
||||
saveWebhook(webhook: Webhook): Promise<void>;
|
||||
getWebhook(id: string): Promise<Webhook | null>;
|
||||
listWebhooks(filters?: { enabled?: boolean; event?: string }): Promise<Webhook[]>;
|
||||
updateWebhook(id: string, updates: Partial<Webhook>): Promise<Webhook>;
|
||||
deleteWebhook(id: string): Promise<void>;
|
||||
|
||||
// Delivery attempt operations
|
||||
saveDeliveryAttempt(attempt: DeliveryAttempt): Promise<void>;
|
||||
getDeliveryAttempts(webhookId: string, limit?: number): Promise<DeliveryAttempt[]>;
|
||||
|
||||
// DLQ operations
|
||||
saveToDLQ(entry: DLQEntry): Promise<void>;
|
||||
getDLQEntries(limit?: number, offset?: number): Promise<{ entries: DLQEntry[]; total: number }>;
|
||||
removeFromDLQ(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory storage implementation
|
||||
*/
|
||||
class InMemoryStorage implements StorageAdapter {
|
||||
private webhooks = new Map<string, Webhook>();
|
||||
private deliveryAttempts = new Map<string, DeliveryAttempt>();
|
||||
private dlq = new Map<string, DLQEntry>();
|
||||
|
||||
async saveWebhook(webhook: Webhook): Promise<void> {
|
||||
this.webhooks.set(webhook.id, { ...webhook });
|
||||
}
|
||||
|
||||
async getWebhook(id: string): Promise<Webhook | null> {
|
||||
return this.webhooks.get(id) || null;
|
||||
}
|
||||
|
||||
async listWebhooks(filters?: { enabled?: boolean; event?: string }): Promise<Webhook[]> {
|
||||
let webhooks = Array.from(this.webhooks.values());
|
||||
|
||||
if (filters?.enabled !== undefined) {
|
||||
webhooks = webhooks.filter(w => w.enabled === filters.enabled);
|
||||
}
|
||||
|
||||
if (filters?.event) {
|
||||
webhooks = webhooks.filter(w => w.events.includes(filters.event!));
|
||||
}
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
async updateWebhook(id: string, updates: Partial<Webhook>): Promise<Webhook> {
|
||||
const webhook = this.webhooks.get(id);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...webhook,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
this.webhooks.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteWebhook(id: string): Promise<void> {
|
||||
if (!this.webhooks.has(id)) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
this.webhooks.delete(id);
|
||||
}
|
||||
|
||||
async saveDeliveryAttempt(attempt: DeliveryAttempt): Promise<void> {
|
||||
this.deliveryAttempts.set(attempt.id, { ...attempt });
|
||||
}
|
||||
|
||||
async getDeliveryAttempts(webhookId: string, limit: number = 50): Promise<DeliveryAttempt[]> {
|
||||
const attempts = Array.from(this.deliveryAttempts.values())
|
||||
.filter(a => a.webhookId === webhookId)
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.slice(0, limit);
|
||||
|
||||
return attempts;
|
||||
}
|
||||
|
||||
async saveToDLQ(entry: DLQEntry): Promise<void> {
|
||||
this.dlq.set(entry.id, { ...entry });
|
||||
}
|
||||
|
||||
async getDLQEntries(limit: number = 20, offset: number = 0): Promise<{ entries: DLQEntry[]; total: number }> {
|
||||
const entries = Array.from(this.dlq.values())
|
||||
.sort((a, b) => b.failedAt - a.failedAt);
|
||||
|
||||
const total = entries.length;
|
||||
const paginated = entries.slice(offset, offset + limit);
|
||||
|
||||
return { entries: paginated, total };
|
||||
}
|
||||
|
||||
async removeFromDLQ(id: string): Promise<void> {
|
||||
if (!this.dlq.has(id)) {
|
||||
throw new Error(`DLQ entry not found: ${id}`);
|
||||
}
|
||||
this.dlq.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export const storage: StorageAdapter = new InMemoryStorage();
|
||||
|
||||
@@ -2,44 +2,103 @@
|
||||
* Webhook service - manages webhook registrations
|
||||
*/
|
||||
|
||||
export interface Webhook {
|
||||
id: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
secret?: string;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { storage, Webhook } from './storage';
|
||||
import { webhookDeliveryService } from './delivery';
|
||||
|
||||
export const webhookService = {
|
||||
async createWebhook(data: Partial<Webhook>): Promise<Webhook> {
|
||||
// TODO: Store webhook in database
|
||||
throw new Error('Not implemented');
|
||||
if (!data.url || !data.events || data.events.length === 0) {
|
||||
throw new Error('Missing required fields: url, events');
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
try {
|
||||
new URL(data.url);
|
||||
} catch {
|
||||
throw new Error('Invalid webhook URL');
|
||||
}
|
||||
|
||||
const webhook: Webhook = {
|
||||
id: uuidv4(),
|
||||
url: data.url,
|
||||
events: data.events,
|
||||
secret: data.secret,
|
||||
enabled: data.enabled !== undefined ? data.enabled : true,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
metadata: data.metadata,
|
||||
};
|
||||
|
||||
await storage.saveWebhook(webhook);
|
||||
return webhook;
|
||||
},
|
||||
|
||||
async updateWebhook(id: string, data: Partial<Webhook>): Promise<Webhook> {
|
||||
// TODO: Update webhook in database
|
||||
throw new Error('Not implemented');
|
||||
const webhook = await storage.getWebhook(id);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
// Validate URL if provided
|
||||
if (data.url) {
|
||||
try {
|
||||
new URL(data.url);
|
||||
} catch {
|
||||
throw new Error('Invalid webhook URL');
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await storage.updateWebhook(id, data);
|
||||
return updated;
|
||||
},
|
||||
|
||||
async getWebhook(id: string): Promise<Webhook> {
|
||||
// TODO: Retrieve webhook from database
|
||||
throw new Error('Not implemented');
|
||||
const webhook = await storage.getWebhook(id);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
return webhook;
|
||||
},
|
||||
|
||||
async listWebhooks(): Promise<Webhook[]> {
|
||||
// TODO: List all webhooks
|
||||
throw new Error('Not implemented');
|
||||
async listWebhooks(filters?: { enabled?: boolean; event?: string }): Promise<Webhook[]> {
|
||||
return await storage.listWebhooks(filters);
|
||||
},
|
||||
|
||||
async deleteWebhook(id: string): Promise<void> {
|
||||
await storage.deleteWebhook(id);
|
||||
},
|
||||
|
||||
async testWebhook(id: string): Promise<void> {
|
||||
// TODO: Send test event to webhook
|
||||
throw new Error('Not implemented');
|
||||
const webhook = await storage.getWebhook(id);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
// Send test event
|
||||
const testEvent = {
|
||||
eventId: uuidv4(),
|
||||
eventType: 'webhook.test',
|
||||
occurredAt: new Date().toISOString(),
|
||||
payload: {
|
||||
message: 'This is a test webhook event',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
await webhookDeliveryService.deliver(id, webhook.url, testEvent, webhook.secret);
|
||||
},
|
||||
|
||||
async replayWebhooks(id: string, since?: string): Promise<number> {
|
||||
// TODO: Replay events since timestamp
|
||||
throw new Error('Not implemented');
|
||||
const webhook = await storage.getWebhook(id);
|
||||
if (!webhook) {
|
||||
throw new Error(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
// In production, this would query event store for events since timestamp
|
||||
// For now, return 0 (no events to replay)
|
||||
// This would require integration with event store/event bus history
|
||||
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user