Files
bunq/ts/bunq.classes.webhook.ts
Juergen Kunz 596efa3f06 update
2025-07-18 10:43:39 +00:00

309 lines
7.9 KiB
TypeScript

import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js';
import { BunqNotification, BunqWebhookHandler } from './bunq.classes.notification.js';
import { BunqCrypto } from './bunq.classes.crypto.js';
/**
* Webhook server for receiving bunq notifications
*/
export class BunqWebhookServer {
private bunqAccount: BunqAccount;
private notification: BunqNotification;
private handler: BunqWebhookHandler;
private server?: any; // HTTP server instance
private port: number;
private path: string;
private publicUrl: string;
constructor(
bunqAccount: BunqAccount,
options: {
port?: number;
path?: string;
publicUrl: string;
}
) {
this.bunqAccount = bunqAccount;
this.notification = new BunqNotification(bunqAccount);
this.handler = new BunqWebhookHandler();
this.port = options.port || 3000;
this.path = options.path || '/webhook';
this.publicUrl = options.publicUrl;
}
/**
* Start the webhook server
*/
public async start(): Promise<void> {
// Create HTTP server
const http = await import('http');
this.server = http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url === this.path) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
// Get signature from headers
const signature = req.headers['x-bunq-server-signature'] as string;
if (!signature) {
res.statusCode = 401;
res.end('Missing signature');
return;
}
// Verify signature
const isValid = this.notification.verifyWebhookSignature(body, signature);
if (!isValid) {
res.statusCode = 401;
res.end('Invalid signature');
return;
}
// Parse and process notification
const notification = JSON.parse(body);
await this.handler.process(notification);
res.statusCode = 200;
res.end('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.statusCode = 500;
res.end('Internal server error');
}
});
} else {
res.statusCode = 404;
res.end('Not found');
}
});
this.server.listen(this.port, () => {
console.log(`Webhook server listening on port ${this.port}`);
});
}
/**
* Stop the webhook server
*/
public async stop(): Promise<void> {
if (this.server) {
await new Promise<void>((resolve) => {
this.server.close(() => {
resolve();
});
});
this.server = undefined;
}
}
/**
* Get the webhook handler
*/
public getHandler(): BunqWebhookHandler {
return this.handler;
}
/**
* Register webhook with bunq
*/
public async register(categories?: string[]): Promise<void> {
const webhookUrl = `${this.publicUrl}${this.path}`;
if (categories && categories.length > 0) {
// Register specific categories
const filters = categories.map(category => ({
category,
notificationTarget: webhookUrl
}));
await this.notification.createMultipleUrlFilters(filters);
} else {
// Register all payment and account events
await this.notification.setupPaymentWebhook(webhookUrl);
await this.notification.setupAccountWebhook(webhookUrl);
}
}
/**
* Unregister all webhooks
*/
public async unregister(): Promise<void> {
await this.notification.clearAllUrlFilters();
}
}
/**
* Webhook client for sending test notifications
*/
export class BunqWebhookClient {
private crypto: BunqCrypto;
private privateKey: string;
constructor(privateKey: string) {
this.crypto = new BunqCrypto();
this.privateKey = privateKey;
}
/**
* Send a test notification to a webhook endpoint
*/
public async sendTestNotification(
webhookUrl: string,
notification: any
): Promise<void> {
const body = JSON.stringify(notification);
// Create signature
const sign = plugins.crypto.createSign('SHA256');
sign.update(body);
sign.end();
const signature = sign.sign(this.privateKey, 'base64');
// Send request
const response = await plugins.smartrequest.request(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Bunq-Server-Signature': signature
},
requestBody: body
});
if (response.statusCode !== 200) {
throw new Error(`Webhook request failed with status ${response.statusCode}`);
}
}
/**
* Create a test payment notification
*/
public createTestPaymentNotification(paymentData: any): any {
return {
NotificationUrl: {
target_url: 'https://example.com/webhook',
category: 'PAYMENT',
event_type: 'PAYMENT_CREATED',
object: {
Payment: {
id: 1234,
created: new Date().toISOString(),
updated: new Date().toISOString(),
monetary_account_id: 1,
amount: {
currency: 'EUR',
value: '10.00'
},
description: 'Test payment',
type: 'IDEAL',
...paymentData
}
}
}
};
}
/**
* Create a test account notification
*/
public createTestAccountNotification(accountData: any): any {
return {
NotificationUrl: {
target_url: 'https://example.com/webhook',
category: 'MONETARY_ACCOUNT',
event_type: 'MONETARY_ACCOUNT_UPDATED',
object: {
MonetaryAccountBank: {
id: 1234,
created: new Date().toISOString(),
updated: new Date().toISOString(),
balance: {
currency: 'EUR',
value: '100.00'
},
...accountData
}
}
}
};
}
}
/**
* Webhook event types
*/
export enum BunqWebhookEventType {
// Payment events
PAYMENT_CREATED = 'PAYMENT_CREATED',
PAYMENT_UPDATED = 'PAYMENT_UPDATED',
PAYMENT_CANCELLED = 'PAYMENT_CANCELLED',
// Account events
MONETARY_ACCOUNT_CREATED = 'MONETARY_ACCOUNT_CREATED',
MONETARY_ACCOUNT_UPDATED = 'MONETARY_ACCOUNT_UPDATED',
MONETARY_ACCOUNT_CLOSED = 'MONETARY_ACCOUNT_CLOSED',
// Card events
CARD_CREATED = 'CARD_CREATED',
CARD_UPDATED = 'CARD_UPDATED',
CARD_CANCELLED = 'CARD_CANCELLED',
CARD_TRANSACTION = 'CARD_TRANSACTION',
// Request events
REQUEST_INQUIRY_CREATED = 'REQUEST_INQUIRY_CREATED',
REQUEST_INQUIRY_UPDATED = 'REQUEST_INQUIRY_UPDATED',
REQUEST_INQUIRY_ACCEPTED = 'REQUEST_INQUIRY_ACCEPTED',
REQUEST_INQUIRY_REJECTED = 'REQUEST_INQUIRY_REJECTED',
// Other events
SCHEDULE_RESULT = 'SCHEDULE_RESULT',
TAB_RESULT = 'TAB_RESULT',
DRAFT_PAYMENT_CREATED = 'DRAFT_PAYMENT_CREATED',
DRAFT_PAYMENT_UPDATED = 'DRAFT_PAYMENT_UPDATED'
}
/**
* Webhook middleware for Express.js
*/
export function bunqWebhookMiddleware(
bunqAccount: BunqAccount,
handler: BunqWebhookHandler
) {
const notification = new BunqNotification(bunqAccount);
return async (req: any, res: any, next: any) => {
try {
// Get signature from headers
const signature = req.headers['x-bunq-server-signature'];
if (!signature) {
res.status(401).send('Missing signature');
return;
}
// Get raw body
const body = JSON.stringify(req.body);
// Verify signature
const isValid = notification.verifyWebhookSignature(body, signature);
if (!isValid) {
res.status(401).send('Invalid signature');
return;
}
// Process notification
await handler.process(req.body);
res.status(200).send('OK');
} catch (error) {
next(error);
}
};
}