2025-07-18 10:43:39 +00:00
|
|
|
import * as plugins from './bunq.plugins.js';
|
|
|
|
import { BunqCrypto } from './bunq.classes.crypto.js';
|
|
|
|
import { BunqSession } from './bunq.classes.session.js';
|
2025-07-25 02:10:16 +00:00
|
|
|
import type { IBunqApiContext, ISessionData } from './bunq.interfaces.js';
|
2025-07-18 10:31:12 +00:00
|
|
|
|
|
|
|
export interface IBunqApiContextOptions {
|
|
|
|
apiKey: string;
|
|
|
|
environment: 'SANDBOX' | 'PRODUCTION';
|
|
|
|
deviceDescription: string;
|
|
|
|
permittedIps?: string[];
|
2025-07-22 21:10:41 +00:00
|
|
|
isOAuthToken?: boolean;
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class BunqApiContext {
|
|
|
|
private options: IBunqApiContextOptions;
|
|
|
|
private crypto: BunqCrypto;
|
|
|
|
private session: BunqSession;
|
|
|
|
private context: IBunqApiContext;
|
|
|
|
|
|
|
|
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'
|
|
|
|
};
|
|
|
|
|
|
|
|
this.session = new BunqSession(this.crypto, this.context);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the API context (installation, device, session)
|
2025-07-25 02:10:16 +00:00
|
|
|
* @returns The session data that can be persisted by the consumer
|
2025-07-18 10:31:12 +00:00
|
|
|
*/
|
2025-07-25 02:10:16 +00:00
|
|
|
public async init(): Promise<ISessionData> {
|
2025-07-18 10:31:12 +00:00
|
|
|
// Create new session
|
|
|
|
await this.session.init(
|
|
|
|
this.options.deviceDescription,
|
|
|
|
this.options.permittedIps || []
|
|
|
|
);
|
2025-07-22 21:56:10 +00:00
|
|
|
|
|
|
|
// Set OAuth mode if applicable (for session expiry handling)
|
|
|
|
if (this.options.isOAuthToken) {
|
|
|
|
this.session.setOAuthMode(true);
|
|
|
|
}
|
2025-07-18 10:31:12 +00:00
|
|
|
|
2025-07-25 02:10:16 +00:00
|
|
|
return this.exportSession();
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-07-25 02:10:16 +00:00
|
|
|
* Initialize the API context with existing session data
|
|
|
|
* @param sessionData The session data to restore
|
2025-07-18 10:31:12 +00:00
|
|
|
*/
|
2025-07-25 02:10:16 +00:00
|
|
|
public async initWithSession(sessionData: ISessionData): Promise<void> {
|
|
|
|
// Validate session data
|
|
|
|
if (!sessionData.sessionToken || !sessionData.sessionId) {
|
|
|
|
throw new Error('Invalid session data: missing session token or ID');
|
|
|
|
}
|
2025-07-18 10:31:12 +00:00
|
|
|
|
2025-07-25 02:10:16 +00:00
|
|
|
// Restore crypto keys
|
|
|
|
this.crypto.setKeys(
|
|
|
|
sessionData.clientPrivateKey,
|
|
|
|
sessionData.clientPublicKey
|
2025-07-18 10:31:12 +00:00
|
|
|
);
|
2025-07-25 02:10:16 +00:00
|
|
|
|
|
|
|
// Update context with session data
|
|
|
|
this.context = {
|
|
|
|
...this.context,
|
|
|
|
sessionToken: sessionData.sessionToken,
|
|
|
|
sessionId: sessionData.sessionId,
|
|
|
|
installationToken: sessionData.installationToken,
|
|
|
|
serverPublicKey: sessionData.serverPublicKey,
|
|
|
|
clientPrivateKey: sessionData.clientPrivateKey,
|
|
|
|
clientPublicKey: sessionData.clientPublicKey,
|
|
|
|
expiresAt: new Date(sessionData.expiresAt)
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create new session instance with restored context
|
|
|
|
this.session = new BunqSession(this.crypto, this.context);
|
|
|
|
|
|
|
|
// Set OAuth mode if applicable
|
|
|
|
if (this.options.isOAuthToken) {
|
|
|
|
this.session.setOAuthMode(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if session is still valid
|
|
|
|
if (!this.session.isSessionValid()) {
|
|
|
|
throw new Error('Session has expired');
|
|
|
|
}
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-07-25 02:10:16 +00:00
|
|
|
* Export the current session data for persistence
|
|
|
|
* @returns The session data that can be saved by the consumer
|
2025-07-18 10:31:12 +00:00
|
|
|
*/
|
2025-07-25 02:10:16 +00:00
|
|
|
public exportSession(): ISessionData {
|
|
|
|
const context = this.session.getContext();
|
|
|
|
|
|
|
|
if (!context.sessionToken || !context.sessionId) {
|
|
|
|
throw new Error('No active session to export');
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
2025-07-25 02:10:16 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
sessionId: context.sessionId,
|
|
|
|
sessionToken: context.sessionToken,
|
|
|
|
installationToken: context.installationToken!,
|
|
|
|
serverPublicKey: context.serverPublicKey!,
|
|
|
|
clientPrivateKey: context.clientPrivateKey!,
|
|
|
|
clientPublicKey: context.clientPublicKey!,
|
|
|
|
expiresAt: context.expiresAt!,
|
|
|
|
environment: context.environment,
|
|
|
|
baseUrl: context.baseUrl
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new BunqApiContext with existing session data
|
|
|
|
* @param sessionData The session data to use
|
|
|
|
* @param apiKey The API key (still needed for refresh)
|
|
|
|
* @param deviceDescription Device description
|
|
|
|
* @returns A new BunqApiContext instance
|
|
|
|
*/
|
|
|
|
public static async createWithSession(
|
|
|
|
sessionData: ISessionData,
|
|
|
|
apiKey: string,
|
|
|
|
deviceDescription: string
|
|
|
|
): Promise<BunqApiContext> {
|
|
|
|
const context = new BunqApiContext({
|
|
|
|
apiKey,
|
|
|
|
environment: sessionData.environment,
|
|
|
|
deviceDescription,
|
|
|
|
isOAuthToken: false // Set appropriately based on your needs
|
|
|
|
});
|
|
|
|
|
|
|
|
await context.initWithSession(sessionData);
|
|
|
|
return context;
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2025-07-25 02:10:16 +00:00
|
|
|
* @returns Updated session data if session was refreshed, null otherwise
|
2025-07-18 10:31:12 +00:00
|
|
|
*/
|
2025-07-25 02:10:16 +00:00
|
|
|
public async ensureValidSession(): Promise<ISessionData | null> {
|
|
|
|
const wasValid = this.session.isSessionValid();
|
2025-07-18 10:31:12 +00:00
|
|
|
await this.session.refreshSession();
|
2025-07-25 02:10:16 +00:00
|
|
|
|
|
|
|
// Return updated session data only if session was actually refreshed
|
|
|
|
if (!wasValid) {
|
|
|
|
return this.exportSession();
|
|
|
|
}
|
|
|
|
return null;
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-07-25 02:10:16 +00:00
|
|
|
* Destroy the current session
|
2025-07-18 10:31:12 +00:00
|
|
|
*/
|
|
|
|
public async destroy(): Promise<void> {
|
|
|
|
await this.session.destroySession();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the environment
|
|
|
|
*/
|
|
|
|
public getEnvironment(): 'SANDBOX' | 'PRODUCTION' {
|
|
|
|
return this.options.environment;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the base URL
|
|
|
|
*/
|
|
|
|
public getBaseUrl(): string {
|
|
|
|
return this.context.baseUrl;
|
|
|
|
}
|
2025-07-22 22:56:50 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the context has a valid session
|
|
|
|
*/
|
|
|
|
public hasValidSession(): boolean {
|
|
|
|
return this.session && this.session.isSessionValid();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-07-24 12:28:50 +00:00
|
|
|
* Initialize with existing installation and device (for OAuth tokens that already completed these steps)
|
2025-07-25 02:10:16 +00:00
|
|
|
* @param existingInstallation Optional partial session data with just installation/device info
|
|
|
|
* @returns The new session data
|
2025-07-22 22:56:50 +00:00
|
|
|
*/
|
2025-07-25 02:10:16 +00:00
|
|
|
public async initWithExistingInstallation(existingInstallation?: Partial<ISessionData>): Promise<ISessionData> {
|
2025-07-24 12:28:50 +00:00
|
|
|
// For OAuth tokens that already have installation/device but need a new session
|
2025-07-22 22:56:50 +00:00
|
|
|
|
2025-07-25 02:10:16 +00:00
|
|
|
if (existingInstallation && existingInstallation.clientPrivateKey && existingInstallation.clientPublicKey) {
|
2025-07-24 12:28:50 +00:00
|
|
|
// Restore crypto keys from previous installation
|
|
|
|
this.crypto.setKeys(
|
2025-07-25 02:10:16 +00:00
|
|
|
existingInstallation.clientPrivateKey,
|
|
|
|
existingInstallation.clientPublicKey
|
2025-07-24 12:28:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Update context with existing installation data
|
2025-07-25 02:10:16 +00:00
|
|
|
this.context = {
|
|
|
|
...this.context,
|
|
|
|
installationToken: existingInstallation.installationToken,
|
|
|
|
serverPublicKey: existingInstallation.serverPublicKey,
|
|
|
|
clientPrivateKey: existingInstallation.clientPrivateKey,
|
|
|
|
clientPublicKey: existingInstallation.clientPublicKey
|
|
|
|
};
|
2025-07-22 22:56:50 +00:00
|
|
|
|
2025-07-24 12:28:50 +00:00
|
|
|
// Create new session instance
|
|
|
|
this.session = new BunqSession(this.crypto, this.context);
|
|
|
|
|
|
|
|
// Try to create a new session with the OAuth token
|
|
|
|
try {
|
|
|
|
await this.session.init(
|
|
|
|
this.options.deviceDescription,
|
|
|
|
this.options.permittedIps || [],
|
|
|
|
true // skipInstallationAndDevice = true
|
|
|
|
);
|
|
|
|
|
|
|
|
if (this.options.isOAuthToken) {
|
|
|
|
this.session.setOAuthMode(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('Successfully created new session with existing installation');
|
2025-07-25 02:10:16 +00:00
|
|
|
return this.exportSession();
|
2025-07-24 12:28:50 +00:00
|
|
|
} catch (error) {
|
|
|
|
throw new Error(`Failed to create session with OAuth token: ${error.message}`);
|
2025-07-22 22:56:50 +00:00
|
|
|
}
|
2025-07-24 12:28:50 +00:00
|
|
|
} else {
|
|
|
|
// No existing installation, fall back to full init
|
2025-07-25 02:10:16 +00:00
|
|
|
throw new Error('No existing installation provided, full initialization required');
|
2025-07-22 22:56:50 +00:00
|
|
|
}
|
|
|
|
}
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|