BREAKING CHANGE(core): implement complete stateless architecture with consumer-controlled session persistence
This commit is contained in:
204
readme.md
204
readme.md
@@ -27,6 +27,25 @@ A powerful, type-safe TypeScript/JavaScript client for the bunq API with full fe
|
||||
- 🛡️ **Type Safety** - Compile-time type checking for all operations
|
||||
- 📚 **Comprehensive Documentation** - Detailed examples for every feature
|
||||
|
||||
## Stateless Architecture (v4.0.0+)
|
||||
|
||||
Starting from version 4.0.0, this library is completely stateless. Session management is now entirely controlled by the consumer:
|
||||
|
||||
### Key Changes
|
||||
- **No File Persistence** - The library no longer saves any state to disk
|
||||
- **Session Data Export** - Full session data is returned for you to persist
|
||||
- **Session Data Import** - Initialize with previously saved session data
|
||||
- **Explicit Session Management** - You control when and how sessions are stored
|
||||
|
||||
### Benefits
|
||||
- **Full Control** - Store sessions in your preferred storage (database, Redis, etc.)
|
||||
- **Better for Microservices** - No shared state between instances
|
||||
- **Improved Testing** - Predictable behavior with no hidden state
|
||||
- **Enhanced Security** - You control where sensitive data is stored
|
||||
|
||||
### Migration from v3.x
|
||||
If you're upgrading from v3.x, you'll need to handle session persistence yourself. See the [Stateless Session Management](#stateless-session-management) section for examples.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -53,13 +72,21 @@ const bunq = new BunqAccount({
|
||||
environment: 'PRODUCTION' // or 'SANDBOX' for testing
|
||||
});
|
||||
|
||||
// Initialize connection
|
||||
await bunq.init();
|
||||
// Initialize connection and get session data
|
||||
const sessionData = await bunq.init();
|
||||
|
||||
// IMPORTANT: Save the session data for reuse
|
||||
await saveSessionToDatabase(sessionData);
|
||||
|
||||
// Get your accounts
|
||||
const accounts = await bunq.getAccounts();
|
||||
const { accounts, sessionData: updatedSession } = await bunq.getAccounts();
|
||||
console.log(`Found ${accounts.length} accounts`);
|
||||
|
||||
// If session was refreshed, save the updated data
|
||||
if (updatedSession) {
|
||||
await saveSessionToDatabase(updatedSession);
|
||||
}
|
||||
|
||||
// Get recent transactions
|
||||
const transactions = await accounts[0].getTransactions();
|
||||
transactions.forEach(tx => {
|
||||
@@ -380,13 +407,73 @@ await new ExportBuilder(bunq, account)
|
||||
.downloadTo('/path/to/statement-with-attachments.pdf');
|
||||
```
|
||||
|
||||
### User & Session Management
|
||||
### Stateless Session Management
|
||||
|
||||
```typescript
|
||||
// Initial session creation
|
||||
const bunq = new BunqAccount({
|
||||
apiKey: 'your-api-key',
|
||||
deviceName: 'My App',
|
||||
environment: 'PRODUCTION'
|
||||
});
|
||||
|
||||
// Initialize and receive session data
|
||||
const sessionData = await bunq.init();
|
||||
|
||||
// Save to your preferred storage
|
||||
await saveToDatabase({
|
||||
userId: 'user123',
|
||||
sessionData: sessionData,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
// Reusing existing session
|
||||
const savedData = await loadFromDatabase('user123');
|
||||
const bunq2 = new BunqAccount({
|
||||
apiKey: 'your-api-key',
|
||||
deviceName: 'My App',
|
||||
environment: 'PRODUCTION'
|
||||
});
|
||||
|
||||
// Initialize with saved session
|
||||
await bunq2.initWithSession(savedData.sessionData);
|
||||
|
||||
// Check if session is valid
|
||||
if (!bunq2.isSessionValid()) {
|
||||
// Session expired, create new one
|
||||
const newSession = await bunq2.init();
|
||||
await saveToDatabase({ userId: 'user123', sessionData: newSession });
|
||||
}
|
||||
|
||||
// Making API calls with automatic refresh
|
||||
const { accounts, sessionData: refreshedSession } = await bunq2.getAccounts();
|
||||
|
||||
// Always save refreshed session data
|
||||
if (refreshedSession) {
|
||||
await saveToDatabase({
|
||||
userId: 'user123',
|
||||
sessionData: refreshedSession,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Get current session data at any time
|
||||
const currentSession = bunq2.getSessionData();
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
```typescript
|
||||
// Get user information
|
||||
const user = await bunq.getUser();
|
||||
console.log(`Logged in as: ${user.displayName}`);
|
||||
console.log(`User type: ${user.type}`); // UserPerson, UserCompany, etc.
|
||||
const userInfo = await user.getInfo();
|
||||
|
||||
// Determine user type
|
||||
if (userInfo.UserPerson) {
|
||||
console.log(`Personal account: ${userInfo.UserPerson.display_name}`);
|
||||
} else if (userInfo.UserCompany) {
|
||||
console.log(`Business account: ${userInfo.UserCompany.name}`);
|
||||
}
|
||||
|
||||
// Update user settings
|
||||
await user.update({
|
||||
@@ -395,21 +482,6 @@ await user.update({
|
||||
{ category: 'PAYMENT', notificationDeliveryMethod: 'PUSH' }
|
||||
]
|
||||
});
|
||||
|
||||
// Session management
|
||||
const session = bunq.apiContext.getSession();
|
||||
console.log(`Session expires: ${session.expiryTime}`);
|
||||
|
||||
// Manual session refresh
|
||||
await bunq.apiContext.refreshSession();
|
||||
|
||||
// Save session for later use
|
||||
const sessionData = bunq.apiContext.exportSession();
|
||||
await fs.writeFile('bunq-session.json', JSON.stringify(sessionData));
|
||||
|
||||
// Restore session
|
||||
const savedSession = JSON.parse(await fs.readFile('bunq-session.json'));
|
||||
bunq.apiContext.importSession(savedSession);
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
@@ -436,19 +508,34 @@ const bunq = new BunqAccount({
|
||||
apiKey: 'your-oauth-access-token', // OAuth token from bunq OAuth flow
|
||||
deviceName: 'OAuth App',
|
||||
environment: 'PRODUCTION',
|
||||
isOAuthToken: true // Optional: Set for OAuth-specific handling
|
||||
isOAuthToken: true // Important for OAuth-specific handling
|
||||
});
|
||||
|
||||
await bunq.init();
|
||||
try {
|
||||
// Try normal initialization
|
||||
const sessionData = await bunq.init();
|
||||
await saveOAuthSession(sessionData);
|
||||
} catch (error) {
|
||||
// OAuth token may already have installation/device
|
||||
if (error.message.includes('already has a user session')) {
|
||||
// Load existing installation data if available
|
||||
const existingInstallation = await loadOAuthInstallation();
|
||||
|
||||
// Initialize with existing installation
|
||||
const sessionData = await bunq.initOAuthWithExistingInstallation(existingInstallation);
|
||||
await saveOAuthSession(sessionData);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth tokens work just like regular API keys:
|
||||
// Use the OAuth-initialized account normally
|
||||
const { accounts, sessionData } = await bunq.getAccounts();
|
||||
|
||||
// OAuth tokens work like regular API keys:
|
||||
// 1. They go through installation → device → session creation
|
||||
// 2. The OAuth token is used as the 'secret' during authentication
|
||||
// 3. A session token is created and used for all API calls
|
||||
const accounts = await bunq.getAccounts();
|
||||
|
||||
// According to bunq documentation:
|
||||
// "Just use the OAuth Token (access_token) as a normal bunq API key"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
@@ -590,6 +677,67 @@ await bunqJSClient.registerSession();
|
||||
await bunq.init();
|
||||
```
|
||||
|
||||
## Migration Guide from v3.x to v4.0.0
|
||||
|
||||
Version 4.0.0 introduces a breaking change: the library is now completely stateless. Here's how to migrate:
|
||||
|
||||
### Before (v3.x)
|
||||
```typescript
|
||||
// Session was automatically saved to .nogit/bunqproduction.json
|
||||
const bunq = new BunqAccount({ apiKey, deviceName, environment });
|
||||
await bunq.init(); // Session saved to disk automatically
|
||||
const accounts = await bunq.getAccounts(); // Returns accounts directly
|
||||
```
|
||||
|
||||
### After (v4.0.0)
|
||||
```typescript
|
||||
// You must handle session persistence yourself
|
||||
const bunq = new BunqAccount({ apiKey, deviceName, environment });
|
||||
const sessionData = await bunq.init(); // Returns session data
|
||||
await myDatabase.save('session', sessionData); // You save it
|
||||
|
||||
// API calls now return both data and potentially refreshed session
|
||||
const { accounts, sessionData: newSession } = await bunq.getAccounts();
|
||||
if (newSession) {
|
||||
await myDatabase.save('session', newSession); // Save refreshed session
|
||||
}
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
1. **No automatic file persistence** - Remove any dependency on `.nogit/` files
|
||||
2. **`init()` returns session data** - You must save this data yourself
|
||||
3. **API methods return objects** - Methods like `getAccounts()` now return `{ accounts, sessionData? }`
|
||||
4. **Session reuse requires explicit loading** - Use `initWithSession(savedData)`
|
||||
5. **OAuth handling is explicit** - Use `initOAuthWithExistingInstallation()` for OAuth tokens with existing installations
|
||||
|
||||
### Session Storage Example
|
||||
```typescript
|
||||
// Simple file-based storage (similar to v3.x behavior)
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
async function saveSession(data: ISessionData) {
|
||||
await fs.writeFile('./my-session.json', JSON.stringify(data));
|
||||
}
|
||||
|
||||
async function loadSession(): Promise<ISessionData | null> {
|
||||
try {
|
||||
const data = await fs.readFile('./my-session.json', 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Database storage example
|
||||
async function saveSessionToDB(userId: string, data: ISessionData) {
|
||||
await db.collection('bunq_sessions').updateOne(
|
||||
{ userId },
|
||||
{ $set: { sessionData: data, updatedAt: new Date() } },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The library includes comprehensive test coverage:
|
||||
|
Reference in New Issue
Block a user