import * as plugins from './bunq.plugins.js'; import { BunqHttpClient } from './bunq.classes.httpclient.js'; import { BunqCrypto } from './bunq.classes.crypto.js'; import type { IBunqApiContext, IBunqInstallationResponse, IBunqDeviceServerResponse, IBunqSessionServerResponse } from './bunq.interfaces.js'; export class BunqSession { private httpClient: BunqHttpClient; private crypto: BunqCrypto; private context: IBunqApiContext; private sessionExpiryTime: plugins.smarttime.TimeStamp; private isOAuthMode: boolean = false; constructor(crypto: BunqCrypto, context: IBunqApiContext) { this.crypto = crypto; this.context = context; this.httpClient = new BunqHttpClient(crypto, context); } /** * Initialize a new bunq API session */ public async init(deviceDescription: string, permittedIps: string[] = [], skipInstallationAndDevice: boolean = false): Promise { if (!skipInstallationAndDevice) { // Step 1: Installation await this.createInstallation(); // Step 2: Device registration await this.registerDevice(deviceDescription, permittedIps); } // Step 3: Session creation (always required) await this.createSession(); } /** * Create installation and exchange keys */ private async createInstallation(): Promise { // Generate RSA key pair if not already generated try { this.crypto.getPublicKey(); } catch (error) { await this.crypto.generateKeyPair(); } const response = await this.httpClient.post('/v1/installation', { client_public_key: this.crypto.getPublicKey() }); // Extract installation token and server public key let installationToken: string; let serverPublicKey: string; for (const item of response.Response) { if (item.Token) { installationToken = item.Token.token; } if (item.ServerPublicKey) { serverPublicKey = item.ServerPublicKey.server_public_key; } } if (!installationToken || !serverPublicKey) { throw new Error('Failed to get installation token or server public key'); } // Update context this.context.installationToken = installationToken; this.context.serverPublicKey = serverPublicKey; this.context.clientPrivateKey = this.crypto.getPrivateKey(); this.context.clientPublicKey = this.crypto.getPublicKey(); // Update HTTP client context this.httpClient.updateContext({ installationToken, serverPublicKey }); } /** * Register the device */ private async registerDevice(description: string, permittedIps: string[] = []): Promise { // If no IPs specified, allow all IPs with wildcard const ips = permittedIps.length > 0 ? permittedIps : ['*']; const response = await this.httpClient.post('/v1/device-server', { description, secret: this.context.apiKey, permitted_ips: ips }); // Device is now registered if (!response.Response || !response.Response[0] || !response.Response[0].Id) { throw new Error('Failed to register device'); } } /** * Create a new session */ private async createSession(): Promise { const response = await this.httpClient.post('/v1/session-server', { secret: this.context.apiKey }); // Extract session token, session ID, and user info let sessionToken: string; let sessionId: number; let userId: number; for (const item of response.Response) { if (item.Id) { sessionId = item.Id.id; } if (item.Token) { sessionToken = item.Token.token; } if (item.UserPerson) { userId = item.UserPerson.id; } else if (item.UserCompany) { userId = item.UserCompany.id; } else if (item.UserApiKey) { userId = item.UserApiKey.id; } } if (!sessionToken || !userId || !sessionId) { throw new Error('Failed to create session'); } // Update context this.context.sessionToken = sessionToken; this.context.sessionId = sessionId; // Update HTTP client context this.httpClient.updateContext({ sessionToken }); // Set session expiry (bunq sessions expire after 10 minutes of inactivity) this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 600000); this.context.expiresAt = new Date(Date.now() + 600000); } /** * Set OAuth mode */ public setOAuthMode(isOAuth: boolean): void { this.isOAuthMode = isOAuth; if (isOAuth) { // OAuth tokens don't expire in the same way as regular sessions // Set a far future expiry time const farFutureTime = Date.now() + 365 * 24 * 60 * 60 * 1000; this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(farFutureTime); this.context.expiresAt = new Date(farFutureTime); } } /** * Check if session is still valid */ public isSessionValid(): boolean { if (!this.sessionExpiryTime) { return false; } const now = new plugins.smarttime.TimeStamp(); return now.isOlderThan(this.sessionExpiryTime); } /** * Refresh the session if needed */ public async refreshSession(): Promise { if (!this.isSessionValid()) { await this.createSession(); } } /** * Destroy the current session */ public async destroySession(): Promise { if (this.context.sessionToken) { try { await this.httpClient.delete('/v1/session/' + this.getSessionId()); } catch (error) { // Ignore errors when destroying session } this.context.sessionToken = null; this.httpClient.updateContext({ sessionToken: null }); } } /** * Get the current session ID */ private getSessionId(): string { if (!this.context.sessionId) { throw new Error('Session ID not available'); } return this.context.sessionId.toString(); } /** * Get the HTTP client for making API requests */ public getHttpClient(): BunqHttpClient { return this.httpClient; } /** * Get the current context */ public getContext(): IBunqApiContext { return this.context; } }