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, ISessionData } 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 { 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 * @returns The session data that can be persisted by the consumer */ public async init(): Promise { // Create API context for both OAuth tokens and regular API keys this.apiContext = new BunqApiContext({ apiKey: this.options.apiKey, environment: this.options.environment, deviceDescription: this.options.deviceName, permittedIps: this.options.permittedIps, isOAuthToken: this.options.isOAuthToken }); let sessionData: ISessionData; try { sessionData = await this.apiContext.init(); } catch (error) { // Handle "Superfluous authentication" or "Authentication token already has a user session" errors if (error instanceof BunqApiError && this.options.isOAuthToken) { 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 sessionData = await this.apiContext.initWithExistingInstallation(); } else { throw error; } } else { throw error; } } // Create user instance this.bunqUser = new BunqUser(this.apiContext); // Get user info await this.getUserInfo(); return sessionData; } /** * Initialize the bunq account with existing session data * @param sessionData The session data to restore */ public async initWithSession(sessionData: ISessionData): Promise { // Create API context with existing session this.apiContext = await BunqApiContext.createWithSession( sessionData, this.options.apiKey, this.options.deviceName ); // 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 * @returns An array of monetary accounts and updated session data if session was refreshed */ public async getAccounts(): Promise<{ accounts: BunqMonetaryAccount[], sessionData?: ISessionData }> { const sessionData = 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 { accounts: accountsArray, sessionData: sessionData || undefined }; } /** * Get a specific monetary account * @returns The monetary account and updated session data if session was refreshed */ public async getAccount(accountId: number): Promise<{ account: BunqMonetaryAccount, sessionData?: ISessionData }> { const sessionData = await this.apiContext.ensureValidSession(); const response = await this.apiContext.getHttpClient().get( `/v1/user/${this.userId}/monetary-account/${accountId}` ); if (response.Response && response.Response[0]) { const account = BunqMonetaryAccount.fromAPIObject(this, response.Response[0]); return { account, sessionData: sessionData || undefined }; } 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(); } /** * Get the current session data for persistence * @returns The current session data */ public getSessionData(): ISessionData { return this.apiContext.exportSession(); } /** * Try to initialize with OAuth token using existing installation * This is useful when you know the OAuth token already has installation/device * @param existingInstallation Optional partial session data with installation info * @returns The session data */ public async initOAuthWithExistingInstallation(existingInstallation?: Partial): Promise { if (!this.options.isOAuthToken) { throw new Error('This method is only for OAuth tokens'); } // Create API context this.apiContext = new BunqApiContext({ apiKey: this.options.apiKey, environment: this.options.environment, deviceDescription: this.options.deviceName, permittedIps: this.options.permittedIps, isOAuthToken: true }); // Initialize with existing installation const sessionData = await this.apiContext.initWithExistingInstallation(existingInstallation); // Create user instance this.bunqUser = new BunqUser(this.apiContext); // Get user info await this.getUserInfo(); return sessionData; } /** * Check if the current session is valid * @returns True if session is valid */ public isSessionValid(): boolean { return this.apiContext && this.apiContext.hasValidSession(); } /** * Stop the bunq account and clean up */ public async stop() { if (this.apiContext) { await this.apiContext.destroy(); this.apiContext = null; } } }