update
This commit is contained in:
206
ts/bunq.classes.httpclient.ts
Normal file
206
ts/bunq.classes.httpclient.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
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<IBunqApiContext>): void {
|
||||
this.context = { ...this.context, ...context };
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to bunq
|
||||
*/
|
||||
public async request<T = any>(options: IBunqRequestOptions): Promise<T> {
|
||||
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<T = any>(endpoint: string, params?: any): Promise<T> {
|
||||
return this.request<T>({
|
||||
method: 'LIST',
|
||||
endpoint,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request helper
|
||||
*/
|
||||
public async get<T = any>(endpoint: string): Promise<T> {
|
||||
return this.request<T>({
|
||||
method: 'GET',
|
||||
endpoint
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request helper
|
||||
*/
|
||||
public async post<T = any>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.request<T>({
|
||||
method: 'POST',
|
||||
endpoint,
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request helper
|
||||
*/
|
||||
public async put<T = any>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.request<T>({
|
||||
method: 'PUT',
|
||||
endpoint,
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request helper
|
||||
*/
|
||||
public async delete<T = any>(endpoint: string): Promise<T> {
|
||||
return this.request<T>({
|
||||
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<any>) {
|
||||
const message = errors.map(e => e.error_description).join('; ');
|
||||
super(message);
|
||||
this.name = 'BunqApiError';
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user