import * as plugins from './bunq.plugins.js'; import * as paths from './bunq.paths.js'; import { BunqCrypto } from './bunq.classes.crypto.js'; import { BunqSession } from './bunq.classes.session.js'; import type { IBunqApiContext } from './bunq.interfaces.js'; export interface IBunqApiContextOptions { apiKey: string; environment: 'SANDBOX' | 'PRODUCTION'; deviceDescription: string; permittedIps?: string[]; isOAuthToken?: boolean; } export class BunqApiContext { private options: IBunqApiContextOptions; private crypto: BunqCrypto; private session: BunqSession; private context: IBunqApiContext; private contextFilePath: string; constructor(options: IBunqApiContextOptions) { this.options = options; this.crypto = new BunqCrypto(); // Initialize context this.context = { apiKey: options.apiKey, environment: options.environment, baseUrl: options.environment === 'PRODUCTION' ? 'https://api.bunq.com' : 'https://public-api.sandbox.bunq.com' }; // Set context file path based on environment this.contextFilePath = options.environment === 'PRODUCTION' ? paths.bunqJsonProductionFile : paths.bunqJsonSandboxFile; this.session = new BunqSession(this.crypto, this.context); } /** * Initialize the API context (installation, device, session) */ public async init(): Promise { // Try to load existing context const existingContext = await this.loadContext(); if (existingContext && existingContext.sessionToken) { // Restore crypto keys this.crypto.setKeys( existingContext.clientPrivateKey, existingContext.clientPublicKey ); // Update context this.context = { ...this.context, ...existingContext }; this.session = new BunqSession(this.crypto, this.context); // Check if session is still valid if (this.session.isSessionValid()) { return; } } // Create new session await this.session.init( this.options.deviceDescription, this.options.permittedIps || [] ); // Set OAuth mode if applicable (for session expiry handling) if (this.options.isOAuthToken) { this.session.setOAuthMode(true); } // Save context await this.saveContext(); } /** * Save the current context to file */ private async saveContext(): Promise { await plugins.smartfile.fs.ensureDir(paths.nogitDir); const contextToSave = { ...this.session.getContext(), savedAt: new Date().toISOString() }; await plugins.smartfile.memory.toFs( JSON.stringify(contextToSave, null, 2), this.contextFilePath ); } /** * Load context from file */ private async loadContext(): Promise { try { const exists = await plugins.smartfile.fs.fileExists(this.contextFilePath); if (!exists) { return null; } const contextData = await plugins.smartfile.fs.toStringSync(this.contextFilePath); return JSON.parse(contextData); } catch (error) { return null; } } /** * Get the current session */ public getSession(): BunqSession { return this.session; } /** * Get the HTTP client for making API requests */ public getHttpClient() { return this.session.getHttpClient(); } /** * Refresh session if needed */ public async ensureValidSession(): Promise { await this.session.refreshSession(); await this.saveContext(); } /** * Destroy the current session and clean up */ public async destroy(): Promise { await this.session.destroySession(); // Remove saved context try { await plugins.smartfile.fs.remove(this.contextFilePath); } catch (error) { // Ignore errors when removing file } } /** * Get the environment */ public getEnvironment(): 'SANDBOX' | 'PRODUCTION' { return this.options.environment; } /** * Get the base URL */ public getBaseUrl(): string { return this.context.baseUrl; } /** * Check if the context has a valid session */ public hasValidSession(): boolean { return this.session && this.session.isSessionValid(); } /** * Initialize with existing OAuth session (skip installation/device/session creation) */ public async initWithExistingSession(): Promise { // For OAuth tokens that already have a session, we just need to: // 1. Use the OAuth token as the session token // 2. Set OAuth mode for proper expiry handling this.context.sessionToken = this.options.apiKey; // Create session instance with existing token this.session = new BunqSession(this.crypto, this.context); this.session.setOAuthMode(true); // Try to get user info to validate the session try { // This will test if the session is valid const testClient = this.session.getHttpClient(); const response = await testClient.get('/v1/user'); if (response && response.Response) { console.log('Successfully reused existing OAuth session'); await this.saveContext(); } } catch (error) { throw new Error(`Failed to reuse OAuth session: ${error.message}`); } } }