2025-07-18 10:43:39 +00:00
|
|
|
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';
|
2025-07-18 11:33:13 +00:00
|
|
|
import type { IBunqApiContext } 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;
|
|
|
|
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<void> {
|
|
|
|
// 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 || []
|
|
|
|
);
|
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
|
|
|
|
|
|
|
// Save context
|
|
|
|
await this.saveContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the current context to file
|
|
|
|
*/
|
|
|
|
private async saveContext(): Promise<void> {
|
|
|
|
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<IBunqApiContext | null> {
|
|
|
|
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<void> {
|
|
|
|
await this.session.refreshSession();
|
|
|
|
await this.saveContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy the current session and clean up
|
|
|
|
*/
|
|
|
|
public async destroy(): Promise<void> {
|
|
|
|
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;
|
|
|
|
}
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize with existing OAuth session (skip installation/device/session creation)
|
|
|
|
*/
|
|
|
|
public async initWithExistingSession(): Promise<void> {
|
|
|
|
// 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}`);
|
|
|
|
}
|
|
|
|
}
|
2025-07-18 10:31:12 +00:00
|
|
|
}
|