import * as plugins from './bunq.plugins.js'; import { BunqApiContext } from './bunq.classes.apicontext.js'; import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js'; import { BunqUser } from './bunq.classes.user.js'; import { BunqApiError } from './bunq.classes.httpclient.js'; import type { IBunqSessionServerResponse } from './bunq.interfaces.js'; export interface IBunqConstructorOptions { deviceName: string; apiKey: string; environment: 'SANDBOX' | 'PRODUCTION'; permittedIps?: string[]; isOAuthToken?: boolean; // Set to true when using OAuth access token instead of API key } /** * the main bunq account */ export class BunqAccount { // Static cache for OAuth token sessions to prevent multiple authentication attempts private static oauthSessionCache = new Map(); public options: IBunqConstructorOptions; public apiContext: BunqApiContext; public userId: number; public userType: 'UserPerson' | 'UserCompany' | 'UserApiKey'; private bunqUser: BunqUser; constructor(optionsArg: IBunqConstructorOptions) { this.options = optionsArg; } /** * Initialize the bunq account */ public async init() { // For OAuth tokens, check if we already have a cached session if (this.options.isOAuthToken) { const cacheKey = `${this.options.apiKey}_${this.options.environment}`; const cachedContext = BunqAccount.oauthSessionCache.get(cacheKey); if (cachedContext && cachedContext.hasValidSession()) { // Reuse existing session this.apiContext = cachedContext; console.log('Reusing existing OAuth session from cache'); } else { // Create new context and cache it this.apiContext = new BunqApiContext({ apiKey: this.options.apiKey, environment: this.options.environment, deviceDescription: this.options.deviceName, permittedIps: this.options.permittedIps, isOAuthToken: this.options.isOAuthToken }); try { await this.apiContext.init(); // Cache the successfully initialized context BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext); } catch (error) { // Handle "Superfluous authentication" or "Authentication token already has a user session" errors if (error instanceof BunqApiError) { const errorMessages = error.errors.map(e => e.error_description).join(' '); if (errorMessages.includes('Superfluous authentication') || errorMessages.includes('Authentication token already has a user session')) { console.log('OAuth token already has installation/device, attempting to create new session...'); // Try to create a new session with existing installation/device await this.apiContext.initWithExistingInstallation(); // Cache the context with new session BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext); } else { throw error; } } else { throw error; } } } } else { // Regular API key flow this.apiContext = new BunqApiContext({ apiKey: this.options.apiKey, environment: this.options.environment, deviceDescription: this.options.deviceName, permittedIps: this.options.permittedIps, isOAuthToken: this.options.isOAuthToken }); await this.apiContext.init(); } // Create user instance this.bunqUser = new BunqUser(this.apiContext); // Get user info await this.getUserInfo(); } /** * Get user information and ID */ private async getUserInfo() { const userInfo = await this.bunqUser.getInfo(); if (userInfo.UserPerson) { this.userId = userInfo.UserPerson.id; this.userType = 'UserPerson'; } else if (userInfo.UserCompany) { this.userId = userInfo.UserCompany.id; this.userType = 'UserCompany'; } else if (userInfo.UserApiKey) { this.userId = userInfo.UserApiKey.id; this.userType = 'UserApiKey'; } else { throw new Error('Could not determine user type'); } } /** * Get all monetary accounts */ public async getAccounts(): Promise { await this.apiContext.ensureValidSession(); const response = await this.apiContext.getHttpClient().list( `/v1/user/${this.userId}/monetary-account` ); const accountsArray: BunqMonetaryAccount[] = []; if (response.Response) { for (const apiAccount of response.Response) { accountsArray.push(BunqMonetaryAccount.fromAPIObject(this, apiAccount)); } } return accountsArray; } /** * Get a specific monetary account */ public async getAccount(accountId: number): Promise { await this.apiContext.ensureValidSession(); const response = await this.apiContext.getHttpClient().get( `/v1/user/${this.userId}/monetary-account/${accountId}` ); if (response.Response && response.Response[0]) { return BunqMonetaryAccount.fromAPIObject(this, response.Response[0]); } throw new Error('Account not found'); } /** * Create a sandbox user (only works in sandbox environment) */ public async createSandboxUser(): Promise { if (this.options.environment !== 'SANDBOX') { throw new Error('Creating sandbox users only works in sandbox environment'); } // Sandbox user creation doesn't require authentication const response = await plugins.smartrequest.request( 'https://public-api.sandbox.bunq.com/v1/sandbox-user-person', { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'bunq-api-client/1.0.0', 'Cache-Control': 'no-cache' }, requestBody: '{}' } ); if (response.body.Response && response.body.Response[0] && response.body.Response[0].ApiKey) { return response.body.Response[0].ApiKey.api_key; } throw new Error('Failed to create sandbox user'); } /** * Get the user instance */ public getUser(): BunqUser { return this.bunqUser; } /** * Get the HTTP client */ public getHttpClient() { return this.apiContext.getHttpClient(); } /** * Stop the bunq account and clean up */ public async stop() { if (this.apiContext) { await this.apiContext.destroy(); this.apiContext = null; } } /** * Clear the OAuth session cache */ public static clearOAuthCache(): void { BunqAccount.oauthSessionCache.clear(); console.log('OAuth session cache cleared'); } /** * Clear a specific OAuth token from the cache */ public static clearOAuthCacheForToken(apiKey: string, environment: 'SANDBOX' | 'PRODUCTION'): void { const cacheKey = `${apiKey}_${environment}`; BunqAccount.oauthSessionCache.delete(cacheKey); console.log(`OAuth session cache cleared for token in ${environment} environment`); } /** * Get the current size of the OAuth cache */ public static getOAuthCacheSize(): number { return BunqAccount.oauthSessionCache.size; } }