This commit is contained in:
2025-07-18 11:33:13 +00:00
parent 596efa3f06
commit f530fa639a
14 changed files with 54 additions and 64 deletions

View File

@@ -2,7 +2,7 @@ import * as plugins from './bunq.plugins.js';
import { BunqApiContext } from './bunq.classes.apicontext.js'; import { BunqApiContext } from './bunq.classes.apicontext.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import { BunqUser } from './bunq.classes.user.js'; import { BunqUser } from './bunq.classes.user.js';
import { IBunqSessionServerResponse } from './bunq.interfaces.js'; import type { IBunqSessionServerResponse } from './bunq.interfaces.js';
export interface IBunqConstructorOptions { export interface IBunqConstructorOptions {
deviceName: string; deviceName: string;

View File

@@ -2,7 +2,7 @@ import * as plugins from './bunq.plugins.js';
import * as paths from './bunq.paths.js'; import * as paths from './bunq.paths.js';
import { BunqCrypto } from './bunq.classes.crypto.js'; import { BunqCrypto } from './bunq.classes.crypto.js';
import { BunqSession } from './bunq.classes.session.js'; import { BunqSession } from './bunq.classes.session.js';
import { IBunqApiContext } from './bunq.interfaces.js'; import type { IBunqApiContext } from './bunq.interfaces.js';
export interface IBunqApiContextOptions { export interface IBunqApiContextOptions {
apiKey: string; apiKey: string;

View File

@@ -1,6 +1,6 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { IBunqCard, IBunqAmount } from './bunq.interfaces.js'; import type { IBunqCard, IBunqAmount } from './bunq.interfaces.js';
export class BunqCard { export class BunqCard {
private bunqAccount: BunqAccount; private bunqAccount: BunqAccount;

View File

@@ -81,35 +81,7 @@ export class BunqCrypto {
} }
/** /**
* Create the signing string for bunq API requests * Create request signature header (signs only body per bunq docs)
*/
public createSigningString(
method: string,
endpoint: string,
headers: { [key: string]: string },
body: string = ''
): string {
const sortedHeaderNames = Object.keys(headers)
.filter(name => name.startsWith('X-Bunq-') || name === 'Cache-Control' || name === 'User-Agent')
.sort();
let signingString = `${method} ${endpoint}\n`;
for (const headerName of sortedHeaderNames) {
signingString += `${headerName}: ${headers[headerName]}\n`;
}
signingString += '\n';
if (body) {
signingString += body;
}
return signingString;
}
/**
* Create request signature headers
*/ */
public createSignatureHeader( public createSignatureHeader(
method: string, method: string,
@@ -117,12 +89,12 @@ export class BunqCrypto {
headers: { [key: string]: string }, headers: { [key: string]: string },
body: string = '' body: string = ''
): string { ): string {
const signingString = this.createSigningString(method, endpoint, headers, body); // According to bunq docs, only sign the request body
return this.signData(signingString); return this.signData(body);
} }
/** /**
* Verify response signature * Verify response signature (signs only body per bunq API behavior)
*/ */
public verifyResponseSignature( public verifyResponseSignature(
statusCode: number, statusCode: number,
@@ -135,20 +107,8 @@ export class BunqCrypto {
return false; return false;
} }
// Create signing string for response // According to bunq API behavior, only the response body is signed
const sortedHeaderNames = Object.keys(headers) return this.verifyData(body, responseSignature, serverPublicKey);
.filter(name => name.startsWith('x-bunq-') && name !== 'x-bunq-server-signature')
.sort();
let signingString = `${statusCode}\n`;
for (const headerName of sortedHeaderNames) {
signingString += `${headerName}: ${headers[headerName]}\n`;
}
signingString += '\n' + body;
return this.verifyData(signingString, responseSignature, serverPublicKey);
} }
/** /**

View File

@@ -1,7 +1,7 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import { import type {
IBunqPaymentRequest, IBunqPaymentRequest,
IBunqAmount, IBunqAmount,
IBunqAlias, IBunqAlias,

View File

@@ -1,6 +1,6 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqCrypto } from './bunq.classes.crypto.js'; import { BunqCrypto } from './bunq.classes.crypto.js';
import { import type {
IBunqApiContext, IBunqApiContext,
IBunqError, IBunqError,
IBunqRequestOptions IBunqRequestOptions
@@ -77,10 +77,15 @@ export class BunqHttpClient {
} }
} }
// 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.statusCode,
stringHeaders, stringHeaders,
response.body, bodyString,
this.context.serverPublicKey this.context.serverPublicKey
); );
@@ -89,8 +94,18 @@ export class BunqHttpClient {
} }
} }
// Parse response // Parse response - smartrequest may already parse JSON automatically
const responseData = JSON.parse(response.body); let responseData;
if (typeof response.body === 'string') {
try {
responseData = JSON.parse(response.body);
} catch (parseError) {
throw new Error(`Failed to parse JSON response: ${parseError.message}`);
}
} else {
// Response is already parsed
responseData = response.body;
}
// Check for errors // Check for errors
if (responseData.Error) { if (responseData.Error) {
@@ -104,7 +119,15 @@ export class BunqHttpClient {
} }
// Handle network errors // Handle network errors
throw new Error(`Request failed: ${error.message}`); let errorMessage = 'Request failed: ';
if (error instanceof Error) {
errorMessage += error.message;
} else if (typeof error === 'string') {
errorMessage += error;
} else {
errorMessage += JSON.stringify(error);
}
throw new Error(errorMessage);
} }
} }

View File

@@ -2,7 +2,7 @@ import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { BunqTransaction } from './bunq.classes.transaction.js'; import { BunqTransaction } from './bunq.classes.transaction.js';
import { BunqPayment } from './bunq.classes.payment.js'; import { BunqPayment } from './bunq.classes.payment.js';
import { IBunqPaginationOptions, IBunqMonetaryAccountBank } from './bunq.interfaces.js'; import type { IBunqPaginationOptions, IBunqMonetaryAccountBank } from './bunq.interfaces.js';
export type TAccountType = 'joint' | 'savings' | 'bank'; export type TAccountType = 'joint' | 'savings' | 'bank';

View File

@@ -1,6 +1,6 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { IBunqNotificationFilter } from './bunq.interfaces.js'; import type { IBunqNotificationFilter } from './bunq.interfaces.js';
export class BunqNotification { export class BunqNotification {
private bunqAccount: BunqAccount; private bunqAccount: BunqAccount;

View File

@@ -1,7 +1,7 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import { import type {
IBunqPaymentRequest, IBunqPaymentRequest,
IBunqPayment, IBunqPayment,
IBunqAmount, IBunqAmount,

View File

@@ -1,7 +1,7 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import { import type {
IBunqRequestInquiry, IBunqRequestInquiry,
IBunqAmount, IBunqAmount,
IBunqAlias, IBunqAlias,

View File

@@ -1,7 +1,7 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js'; import { BunqAccount } from './bunq.classes.account.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import { import type {
IBunqScheduledPaymentRequest, IBunqScheduledPaymentRequest,
IBunqAmount, IBunqAmount,
IBunqAlias, IBunqAlias,

View File

@@ -1,7 +1,7 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqHttpClient } from './bunq.classes.httpclient.js'; import { BunqHttpClient } from './bunq.classes.httpclient.js';
import { BunqCrypto } from './bunq.classes.crypto.js'; import { BunqCrypto } from './bunq.classes.crypto.js';
import { import type {
IBunqApiContext, IBunqApiContext,
IBunqInstallationResponse, IBunqInstallationResponse,
IBunqDeviceServerResponse, IBunqDeviceServerResponse,
@@ -39,7 +39,9 @@ export class BunqSession {
*/ */
private async createInstallation(): Promise<void> { private async createInstallation(): Promise<void> {
// Generate RSA key pair if not already generated // Generate RSA key pair if not already generated
if (!this.crypto.getPublicKey()) { try {
this.crypto.getPublicKey();
} catch (error) {
await this.crypto.generateKeyPair(); await this.crypto.generateKeyPair();
} }
@@ -143,7 +145,7 @@ export class BunqSession {
} }
const now = new plugins.smarttime.TimeStamp(); const now = new plugins.smarttime.TimeStamp();
return this.sessionExpiryTime.isYoungerThanOtherTimeStamp(now); return now.isOlderThan(this.sessionExpiryTime);
} }
/** /**

View File

@@ -1,6 +1,6 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { BunqApiContext } from './bunq.classes.apicontext.js'; import { BunqApiContext } from './bunq.classes.apicontext.js';
import { IBunqUser } from './bunq.interfaces.js'; import type { IBunqUser } from './bunq.interfaces.js';
export class BunqUser { export class BunqUser {
private apiContext: BunqApiContext; private apiContext: BunqApiContext;

View File

@@ -1,4 +1,9 @@
import * as plugins from './bunq.plugins.js'; import * as plugins from './bunq.plugins.js';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const packageDir = plugins.path.join(__dirname, '../'); export const packageDir = plugins.path.join(__dirname, '../');
export const nogitDir = plugins.path.join(packageDir, './.nogit/'); export const nogitDir = plugins.path.join(packageDir, './.nogit/');