import * as plugins from './bunq.plugins'; import { BunqCrypto } from './bunq.classes.crypto'; import { IBunqApiContext, IBunqError, IBunqRequestOptions } from './bunq.interfaces'; export class BunqHttpClient { private crypto: BunqCrypto; private context: IBunqApiContext; private requestCounter: number = 0; constructor(crypto: BunqCrypto, context: IBunqApiContext) { this.crypto = crypto; this.context = context; } /** * Update the API context (used after getting session token) */ public updateContext(context: Partial): void { this.context = { ...this.context, ...context }; } /** * Make an API request to bunq */ public async request(options: IBunqRequestOptions): Promise { const url = `${this.context.baseUrl}${options.endpoint}`; // Prepare headers const headers = this.prepareHeaders(options); // Prepare body const body = options.body ? JSON.stringify(options.body) : undefined; // Add signature if required if (options.useSigning !== false && this.crypto.getPrivateKey()) { headers['X-Bunq-Client-Signature'] = this.crypto.createSignatureHeader( options.method, options.endpoint, headers, body || '' ); } // Make the request const requestOptions: any = { method: options.method === 'LIST' ? 'GET' : options.method, headers: headers, requestBody: body }; if (options.params) { const params = new URLSearchParams(); Object.entries(options.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { params.append(key, String(value)); } }); requestOptions.queryParams = params.toString(); } try { const response = await plugins.smartrequest.request(url, requestOptions); // Verify response signature if we have server public key if (this.context.serverPublicKey) { // Convert headers to string-only format const stringHeaders: { [key: string]: string } = {}; for (const [key, value] of Object.entries(response.headers)) { if (typeof value === 'string') { stringHeaders[key] = value; } else if (Array.isArray(value)) { stringHeaders[key] = value.join(', '); } } const isValid = this.crypto.verifyResponseSignature( response.statusCode, stringHeaders, response.body, this.context.serverPublicKey ); if (!isValid && options.endpoint !== '/v1/installation') { throw new Error('Invalid response signature'); } } // Parse response const responseData = JSON.parse(response.body); // Check for errors if (responseData.Error) { throw new BunqApiError(responseData.Error); } return responseData; } catch (error) { if (error instanceof BunqApiError) { throw error; } // Handle network errors throw new Error(`Request failed: ${error.message}`); } } /** * Prepare headers for the request */ private prepareHeaders(options: IBunqRequestOptions): { [key: string]: string } { const headers: { [key: string]: string } = { 'Cache-Control': 'no-cache', 'User-Agent': 'bunq-api-client/1.0.0', 'X-Bunq-Language': 'en_US', 'X-Bunq-Region': 'nl_NL', 'X-Bunq-Client-Request-Id': this.crypto.generateRequestId(), 'X-Bunq-Geolocation': '0 0 0 0 NL', 'Content-Type': 'application/json' }; // Add authentication token if (options.useSessionToken !== false) { if (this.context.sessionToken) { headers['X-Bunq-Client-Authentication'] = this.context.sessionToken; } else if (this.context.installationToken && options.endpoint !== '/v1/installation') { headers['X-Bunq-Client-Authentication'] = this.context.installationToken; } } return headers; } /** * LIST request helper */ public async list(endpoint: string, params?: any): Promise { return this.request({ method: 'LIST', endpoint, params }); } /** * GET request helper */ public async get(endpoint: string): Promise { return this.request({ method: 'GET', endpoint }); } /** * POST request helper */ public async post(endpoint: string, body?: any): Promise { return this.request({ method: 'POST', endpoint, body }); } /** * PUT request helper */ public async put(endpoint: string, body?: any): Promise { return this.request({ method: 'PUT', endpoint, body }); } /** * DELETE request helper */ public async delete(endpoint: string): Promise { return this.request({ method: 'DELETE', endpoint }); } } /** * Custom error class for bunq API errors */ export class BunqApiError extends Error { public errors: Array<{ error_description: string; error_description_translated: string; }>; constructor(errors: Array) { const message = errors.map(e => e.error_description).join('; '); super(message); this.name = 'BunqApiError'; this.errors = errors; } }