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 { // 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 { if (this.server) { await new Promise((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 { 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 { 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 { 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); } }; }