From c9fab7def207754bac4f293facb6718cfd9b7f70 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 27 Jul 2025 07:19:34 +0000 Subject: [PATCH] feat(core): switch to native fetch API for all HTTP requests --- changelog.md | 11 ++++++ package.json | 3 +- pnpm-lock.yaml | 3 -- ts/bunq.classes.account.ts | 14 +++++-- ts/bunq.classes.attachment.ts | 27 +++++++++----- ts/bunq.classes.export.ts | 9 ++++- ts/bunq.classes.httpclient.ts | 69 +++++++++++++++++++---------------- ts/bunq.plugins.ts | 3 +- 8 files changed, 85 insertions(+), 54 deletions(-) diff --git a/changelog.md b/changelog.md index 7a40778..0011be3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-07-27 - 4.2.0 - feat(core) +Switch to native fetch API for all HTTP requests + +- Replaced @push.rocks/smartrequest with native fetch API throughout the codebase +- Updated HTTP client to use fetch with proper error handling +- Updated attachment upload/download methods to use fetch +- Updated export download method to use fetch +- Updated sandbox user creation to use fetch +- Removed smartrequest dependency from package.json and plugins +- Improved error messages with HTTP status codes + ## 2025-07-26 - 4.1.3 - fix(export) Fix PDF statement download to use direct content endpoint diff --git a/package.json b/package.json index ec722af..b2834d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apiclient.xyz/bunq", - "version": "4.1.3", + "version": "4.2.0", "private": false, "description": "A full-featured TypeScript/JavaScript client for the bunq API", "type": "module", @@ -33,7 +33,6 @@ "@push.rocks/smartfile": "^11.2.5", "@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpromise": "^4.2.3", - "@push.rocks/smartrequest": "^2.0.21", "@push.rocks/smarttime": "^4.0.54" }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ac7580..011620c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@push.rocks/smartpromise': specifier: ^4.2.3 version: 4.2.3 - '@push.rocks/smartrequest': - specifier: ^2.0.21 - version: 2.1.0 '@push.rocks/smarttime': specifier: ^4.0.54 version: 4.1.1 diff --git a/ts/bunq.classes.account.ts b/ts/bunq.classes.account.ts index c545ffb..a52bb74 100644 --- a/ts/bunq.classes.account.ts +++ b/ts/bunq.classes.account.ts @@ -161,7 +161,7 @@ export class BunqAccount { } // Sandbox user creation doesn't require authentication - const response = await plugins.smartrequest.request( + const response = await fetch( 'https://public-api.sandbox.bunq.com/v1/sandbox-user-person', { method: 'POST', @@ -170,12 +170,18 @@ export class BunqAccount { 'User-Agent': 'bunq-api-client/1.0.0', 'Cache-Control': 'no-cache' }, - requestBody: '{}' + body: '{}' } ); - if (response.body.Response && response.body.Response[0] && response.body.Response[0].ApiKey) { - return response.body.Response[0].ApiKey.api_key; + if (!response.ok) { + throw new Error(`Failed to create sandbox user: HTTP ${response.status}`); + } + + const responseData = await response.json(); + + if (responseData.Response && responseData.Response[0] && responseData.Response[0].ApiKey) { + return responseData.Response[0].ApiKey.api_key; } throw new Error('Failed to create sandbox user'); diff --git a/ts/bunq.classes.attachment.ts b/ts/bunq.classes.attachment.ts index de0174a..5b4b555 100644 --- a/ts/bunq.classes.attachment.ts +++ b/ts/bunq.classes.attachment.ts @@ -47,17 +47,19 @@ export class BunqAttachment { 'X-Bunq-Client-Authentication': this.bunqAccount.apiContext.getSession().getContext().sessionToken }; - const requestOptions = { - method: 'PUT' as const, - headers: headers, - requestBody: options.body - }; - - await plugins.smartrequest.request( + const response = await fetch( `${this.bunqAccount.apiContext.getBaseUrl()}${uploadUrl}`, - requestOptions + { + method: 'PUT', + headers: headers, + body: options.body + } ); + if (!response.ok) { + throw new Error(`Failed to upload attachment: HTTP ${response.status}`); + } + return attachmentUuid; } @@ -67,7 +69,7 @@ export class BunqAttachment { public async getContent(attachmentUuid: string): Promise { await this.bunqAccount.apiContext.ensureValidSession(); - const response = await plugins.smartrequest.request( + const response = await fetch( `${this.bunqAccount.apiContext.getBaseUrl()}/v1/attachment-public/${attachmentUuid}/content`, { method: 'GET', @@ -77,7 +79,12 @@ export class BunqAttachment { } ); - return Buffer.from(response.body); + if (!response.ok) { + throw new Error(`Failed to get attachment: HTTP ${response.status}`); + } + + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); } /** diff --git a/ts/bunq.classes.export.ts b/ts/bunq.classes.export.ts index 2d08621..89a83ec 100644 --- a/ts/bunq.classes.export.ts +++ b/ts/bunq.classes.export.ts @@ -114,14 +114,19 @@ export class BunqExport { // For PDF statements, use the /content endpoint directly const downloadUrl = `${this.bunqAccount.apiContext.getBaseUrl()}/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/customer-statement/${this.id}/content`; - const response = await plugins.smartrequest.request(downloadUrl, { + const response = await fetch(downloadUrl, { method: 'GET', headers: { 'X-Bunq-Client-Authentication': this.bunqAccount.apiContext.getSession().getContext().sessionToken } }); - return Buffer.from(response.body); + if (!response.ok) { + throw new Error(`Failed to download export: HTTP ${response.status}`); + } + + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); } /** diff --git a/ts/bunq.classes.httpclient.ts b/ts/bunq.classes.httpclient.ts index 14feeb9..d0e811f 100644 --- a/ts/bunq.classes.httpclient.ts +++ b/ts/bunq.classes.httpclient.ts @@ -27,7 +27,7 @@ export class BunqHttpClient { * Make an API request to bunq */ public async request(options: IBunqRequestOptions): Promise { - const url = `${this.context.baseUrl}${options.endpoint}`; + let url = `${this.context.baseUrl}${options.endpoint}`; // Prepare headers const headers = this.prepareHeaders(options); @@ -45,47 +45,45 @@ export class BunqHttpClient { ); } - // Make the request - const requestOptions: any = { - method: options.method === 'LIST' ? 'GET' : options.method, - headers: headers, - requestBody: body - }; - + // Handle query parameters if (options.params) { - const queryParams: { [key: string]: string } = {}; + const queryParams = new URLSearchParams(); Object.entries(options.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { - queryParams[key] = String(value); + queryParams.append(key, String(value)); } }); - requestOptions.queryParams = queryParams; + const queryString = queryParams.toString(); + if (queryString) { + url += '?' + queryString; + } } + // Make the request using native fetch + const fetchOptions: RequestInit = { + method: options.method === 'LIST' ? 'GET' : options.method, + headers: headers, + body: body + }; + try { - const response = await plugins.smartrequest.request(url, requestOptions); + const response = await fetch(url, fetchOptions); + + // Get response body as text + const responseText = await response.text(); // 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(', '); - } - } - - // Convert body to string if needed for signature verification - const bodyString = typeof response.body === 'string' - ? response.body - : JSON.stringify(response.body); + response.headers.forEach((value, key) => { + stringHeaders[key] = value; + }); const isValid = this.crypto.verifyResponseSignature( - response.statusCode, + response.status, stringHeaders, - bodyString, + responseText, this.context.serverPublicKey ); @@ -99,17 +97,21 @@ export class BunqHttpClient { } } - // Parse response - smartrequest may already parse JSON automatically + // Parse response let responseData; - if (typeof response.body === 'string') { + if (responseText) { try { - responseData = JSON.parse(response.body); + responseData = JSON.parse(responseText); } catch (parseError) { + // If parsing fails and it's not a 2xx response, throw an HTTP error + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } throw new Error(`Failed to parse JSON response: ${parseError.message}`); } } else { - // Response is already parsed - responseData = response.body; + // Empty response body + responseData = {}; } // Check for errors @@ -117,6 +119,11 @@ export class BunqHttpClient { throw new BunqApiError(responseData.Error); } + // Check HTTP status + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return responseData; } catch (error) { if (error instanceof BunqApiError) { diff --git a/ts/bunq.plugins.ts b/ts/bunq.plugins.ts index c43ba34..52324e9 100644 --- a/ts/bunq.plugins.ts +++ b/ts/bunq.plugins.ts @@ -9,7 +9,6 @@ import * as smartcrypto from '@push.rocks/smartcrypto'; import * as smartfile from '@push.rocks/smartfile'; import * as smartpath from '@push.rocks/smartpath'; import * as smartpromise from '@push.rocks/smartpromise'; -import * as smartrequest from '@push.rocks/smartrequest'; import * as smarttime from '@push.rocks/smarttime'; -export { smartcrypto, smartfile, smartpath, smartpromise, smartrequest, smarttime }; +export { smartcrypto, smartfile, smartpath, smartpromise, smarttime };