feat(core): switch to native fetch API for all HTTP requests
This commit is contained in:
11
changelog.md
11
changelog.md
@@ -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
|
||||||
|
|
||||||
|
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
@@ -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');
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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 };
|
||||||
|
Reference in New Issue
Block a user