feat(core): switch to native fetch API for all HTTP requests

This commit is contained in:
2025-07-27 07:19:34 +00:00
parent fb30c6f4e3
commit c9fab7def2
8 changed files with 85 additions and 54 deletions

View File

@@ -1,5 +1,16 @@
# Changelog # 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) ## 2025-07-26 - 4.1.3 - fix(export)
Fix PDF statement download to use direct content endpoint Fix PDF statement download to use direct content endpoint

View File

@@ -1,6 +1,6 @@
{ {
"name": "@apiclient.xyz/bunq", "name": "@apiclient.xyz/bunq",
"version": "4.1.3", "version": "4.2.0",
"private": false, "private": false,
"description": "A full-featured TypeScript/JavaScript client for the bunq API", "description": "A full-featured TypeScript/JavaScript client for the bunq API",
"type": "module", "type": "module",
@@ -33,7 +33,6 @@
"@push.rocks/smartfile": "^11.2.5", "@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.21",
"@push.rocks/smarttime": "^4.0.54" "@push.rocks/smarttime": "^4.0.54"
}, },
"files": [ "files": [

3
pnpm-lock.yaml generated
View File

@@ -20,9 +20,6 @@ importers:
'@push.rocks/smartpromise': '@push.rocks/smartpromise':
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartrequest':
specifier: ^2.0.21
version: 2.1.0
'@push.rocks/smarttime': '@push.rocks/smarttime':
specifier: ^4.0.54 specifier: ^4.0.54
version: 4.1.1 version: 4.1.1

View File

@@ -161,7 +161,7 @@ export class BunqAccount {
} }
// Sandbox user creation doesn't require authentication // 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', 'https://public-api.sandbox.bunq.com/v1/sandbox-user-person',
{ {
method: 'POST', method: 'POST',
@@ -170,12 +170,18 @@ export class BunqAccount {
'User-Agent': 'bunq-api-client/1.0.0', 'User-Agent': 'bunq-api-client/1.0.0',
'Cache-Control': 'no-cache' 'Cache-Control': 'no-cache'
}, },
requestBody: '{}' body: '{}'
} }
); );
if (response.body.Response && response.body.Response[0] && response.body.Response[0].ApiKey) { if (!response.ok) {
return response.body.Response[0].ApiKey.api_key; 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'); throw new Error('Failed to create sandbox user');

View File

@@ -47,17 +47,19 @@ export class BunqAttachment {
'X-Bunq-Client-Authentication': this.bunqAccount.apiContext.getSession().getContext().sessionToken 'X-Bunq-Client-Authentication': this.bunqAccount.apiContext.getSession().getContext().sessionToken
}; };
const requestOptions = { const response = await fetch(
method: 'PUT' as const,
headers: headers,
requestBody: options.body
};
await plugins.smartrequest.request(
`${this.bunqAccount.apiContext.getBaseUrl()}${uploadUrl}`, `${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; return attachmentUuid;
} }
@@ -67,7 +69,7 @@ export class BunqAttachment {
public async getContent(attachmentUuid: string): Promise<Buffer> { public async getContent(attachmentUuid: string): Promise<Buffer> {
await this.bunqAccount.apiContext.ensureValidSession(); await this.bunqAccount.apiContext.ensureValidSession();
const response = await plugins.smartrequest.request( const response = await fetch(
`${this.bunqAccount.apiContext.getBaseUrl()}/v1/attachment-public/${attachmentUuid}/content`, `${this.bunqAccount.apiContext.getBaseUrl()}/v1/attachment-public/${attachmentUuid}/content`,
{ {
method: 'GET', 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);
} }
/** /**

View File

@@ -114,14 +114,19 @@ export class BunqExport {
// For PDF statements, use the /content endpoint directly // 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 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', method: 'GET',
headers: { headers: {
'X-Bunq-Client-Authentication': this.bunqAccount.apiContext.getSession().getContext().sessionToken '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);
} }
/** /**

View File

@@ -27,7 +27,7 @@ export class BunqHttpClient {
* Make an API request to bunq * Make an API request to bunq
*/ */
public async request<T = any>(options: IBunqRequestOptions): Promise<T> { public async request<T = any>(options: IBunqRequestOptions): Promise<T> {
const url = `${this.context.baseUrl}${options.endpoint}`; let url = `${this.context.baseUrl}${options.endpoint}`;
// Prepare headers // Prepare headers
const headers = this.prepareHeaders(options); const headers = this.prepareHeaders(options);
@@ -45,47 +45,45 @@ export class BunqHttpClient {
); );
} }
// Make the request // Handle query parameters
const requestOptions: any = {
method: options.method === 'LIST' ? 'GET' : options.method,
headers: headers,
requestBody: body
};
if (options.params) { if (options.params) {
const queryParams: { [key: string]: string } = {}; const queryParams = new URLSearchParams();
Object.entries(options.params).forEach(([key, value]) => { Object.entries(options.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) { 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 { 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 // Verify response signature if we have server public key
if (this.context.serverPublicKey) { if (this.context.serverPublicKey) {
// Convert headers to string-only format // Convert headers to string-only format
const stringHeaders: { [key: string]: string } = {}; const stringHeaders: { [key: string]: string } = {};
for (const [key, value] of Object.entries(response.headers)) { response.headers.forEach((value, key) => {
if (typeof value === 'string') { stringHeaders[key] = value;
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);
const isValid = this.crypto.verifyResponseSignature( const isValid = this.crypto.verifyResponseSignature(
response.statusCode, response.status,
stringHeaders, stringHeaders,
bodyString, responseText,
this.context.serverPublicKey this.context.serverPublicKey
); );
@@ -99,17 +97,21 @@ export class BunqHttpClient {
} }
} }
// Parse response - smartrequest may already parse JSON automatically // Parse response
let responseData; let responseData;
if (typeof response.body === 'string') { if (responseText) {
try { try {
responseData = JSON.parse(response.body); responseData = JSON.parse(responseText);
} catch (parseError) { } 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}`); throw new Error(`Failed to parse JSON response: ${parseError.message}`);
} }
} else { } else {
// Response is already parsed // Empty response body
responseData = response.body; responseData = {};
} }
// Check for errors // Check for errors
@@ -117,6 +119,11 @@ export class BunqHttpClient {
throw new BunqApiError(responseData.Error); throw new BunqApiError(responseData.Error);
} }
// Check HTTP status
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return responseData; return responseData;
} catch (error) { } catch (error) {
if (error instanceof BunqApiError) { if (error instanceof BunqApiError) {

View File

@@ -9,7 +9,6 @@ import * as smartcrypto from '@push.rocks/smartcrypto';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smarttime from '@push.rocks/smarttime'; import * as smarttime from '@push.rocks/smarttime';
export { smartcrypto, smartfile, smartpath, smartpromise, smartrequest, smarttime }; export { smartcrypto, smartfile, smartpath, smartpromise, smarttime };