Files
bunq/ts/bunq.classes.session.ts

225 lines
6.2 KiB
TypeScript

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<void> {
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<void> {
// Generate RSA key pair if not already generated
try {
this.crypto.getPublicKey();
} catch (error) {
await this.crypto.generateKeyPair();
}
const response = await this.httpClient.post<IBunqInstallationResponse>('/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<void> {
// If no IPs specified, allow all IPs with wildcard
const ips = permittedIps.length > 0 ? permittedIps : ['*'];
const response = await this.httpClient.post<IBunqDeviceServerResponse>('/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<void> {
const response = await this.httpClient.post<IBunqSessionServerResponse>('/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<void> {
if (!this.isSessionValid()) {
await this.createSession();
}
}
/**
* Destroy the current session
*/
public async destroySession(): Promise<void> {
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;
}
}