BREAKING CHANGE(core): implement complete stateless architecture with consumer-controlled session persistence

This commit is contained in:
2025-07-25 02:10:16 +00:00
parent f790984a95
commit bc0517164f
9 changed files with 594 additions and 140 deletions

View File

@@ -3,7 +3,7 @@ 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 } from './bunq.interfaces.js';
import type { IBunqSessionServerResponse, ISessionData } from './bunq.interfaces.js';
export interface IBunqConstructorOptions {
deviceName: string;
@@ -30,8 +30,9 @@ export class BunqAccount {
/**
* Initialize the bunq account
* @returns The session data that can be persisted by the consumer
*/
public async init() {
public async init(): Promise<ISessionData> {
// Create API context for both OAuth tokens and regular API keys
this.apiContext = new BunqApiContext({
apiKey: this.options.apiKey,
@@ -41,8 +42,10 @@ export class BunqAccount {
isOAuthToken: this.options.isOAuthToken
});
let sessionData: ISessionData;
try {
await this.apiContext.init();
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) {
@@ -51,7 +54,7 @@ export class BunqAccount {
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
await this.apiContext.initWithExistingInstallation();
sessionData = await this.apiContext.initWithExistingInstallation();
} else {
throw error;
}
@@ -65,6 +68,27 @@ export class BunqAccount {
// 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<void> {
// 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();
}
/**
@@ -89,9 +113,10 @@ export class BunqAccount {
/**
* Get all monetary accounts
* @returns An array of monetary accounts and updated session data if session was refreshed
*/
public async getAccounts(): Promise<BunqMonetaryAccount[]> {
await this.apiContext.ensureValidSession();
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`
@@ -105,21 +130,23 @@ export class BunqAccount {
}
}
return accountsArray;
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<BunqMonetaryAccount> {
await this.apiContext.ensureValidSession();
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]) {
return BunqMonetaryAccount.fromAPIObject(this, response.Response[0]);
const account = BunqMonetaryAccount.fromAPIObject(this, response.Response[0]);
return { account, sessionData: sessionData || undefined };
}
throw new Error('Account not found');
@@ -168,6 +195,54 @@ export class BunqAccount {
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<ISessionData>): Promise<ISessionData> {
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
*/