Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
16135cae02 | |||
1190500221 | |||
7cb38acf1e | |||
bc0517164f | |||
f790984a95 |
45
changelog.md
45
changelog.md
@@ -1,5 +1,50 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-07-25 - 4.1.1 - fix(httpclient)
|
||||||
|
Fix query parameter handling for smartrequest compatibility
|
||||||
|
|
||||||
|
- Changed query parameter handling to pass objects directly instead of URLSearchParams
|
||||||
|
- Removed URLSearchParams usage and string conversion
|
||||||
|
- Now passes queryParams as an object to smartrequest which handles URL encoding internally
|
||||||
|
|
||||||
|
## 2025-07-25 - 4.1.0 - feat(transactions)
|
||||||
|
Enhanced transaction pagination support with full control over historical data retrieval
|
||||||
|
|
||||||
|
- Added full `IBunqPaginationOptions` support to `getTransactions()` method
|
||||||
|
- Now supports `older_id` for paginating backwards through historical transactions
|
||||||
|
- Supports custom `count` parameter (defaults to 200)
|
||||||
|
- Maintains backward compatibility - passing a number is still treated as `newer_id`
|
||||||
|
- Only includes pagination parameters that are explicitly set (not false/undefined)
|
||||||
|
- Added `example.pagination.ts` demonstrating various pagination patterns
|
||||||
|
|
||||||
|
This enhancement allows banking applications to properly fetch and paginate through historical transaction data using both `newer_id` and `older_id` parameters.
|
||||||
|
|
||||||
|
## 2025-07-25 - 4.0.0 - BREAKING CHANGE(core)
|
||||||
|
Complete stateless architecture - consumers now have full control over session persistence
|
||||||
|
|
||||||
|
- **BREAKING**: Removed all file-based persistence - no more automatic saving to .nogit/ directory
|
||||||
|
- **BREAKING**: `init()` now returns `ISessionData` that must be persisted by the consumer
|
||||||
|
- **BREAKING**: API methods like `getAccounts()` now return `{ data, sessionData? }` objects
|
||||||
|
- Added `ISessionData` interface exposing complete session state including sessionId
|
||||||
|
- Added `initWithSession(sessionData)` to initialize with previously saved sessions
|
||||||
|
- Added `exportSession()` and `getSessionData()` methods for session access
|
||||||
|
- Added `isSessionValid()` to check session validity
|
||||||
|
- Fixed session destruction to use actual session ID instead of hardcoded '0'
|
||||||
|
- Added `initOAuthWithExistingInstallation()` for explicit OAuth session handling
|
||||||
|
- Session refresh now returns updated session data for consumer persistence
|
||||||
|
- Added `example.stateless.ts` showing session management patterns
|
||||||
|
|
||||||
|
This change gives consumers full control over session persistence strategy (database, Redis, files, etc.) and makes the library suitable for serverless/microservices architectures.
|
||||||
|
|
||||||
|
## 2025-07-22 - 3.1.2 - fix(oauth)
|
||||||
|
Remove OAuth session caching to prevent authentication issues
|
||||||
|
|
||||||
|
- Removed static OAuth session cache that was causing incomplete session issues
|
||||||
|
- Each OAuth token now creates a fresh session without caching
|
||||||
|
- Removed cache management methods (clearOAuthCache, clearOAuthCacheForToken, getOAuthCacheSize)
|
||||||
|
- Simplified init() method to treat OAuth tokens the same as regular API keys
|
||||||
|
- OAuth tokens still handle "Superfluous authentication" errors with initWithExistingInstallation
|
||||||
|
|
||||||
## 2025-07-22 - 3.1.1 - fix(oauth)
|
## 2025-07-22 - 3.1.1 - fix(oauth)
|
||||||
Fix OAuth token authentication flow for existing installations
|
Fix OAuth token authentication flow for existing installations
|
||||||
|
|
||||||
|
128
example.pagination.ts
Normal file
128
example.pagination.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { BunqAccount, IBunqPaginationOptions } from './ts/index.js';
|
||||||
|
|
||||||
|
// Example demonstrating the enhanced pagination support in getTransactions
|
||||||
|
|
||||||
|
async function demonstratePagination() {
|
||||||
|
const bunq = new BunqAccount({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
deviceName: 'Pagination Demo',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize and get session
|
||||||
|
const sessionData = await bunq.init();
|
||||||
|
|
||||||
|
// Get accounts
|
||||||
|
const { accounts } = await bunq.getAccounts();
|
||||||
|
const account = accounts[0];
|
||||||
|
|
||||||
|
// Example 1: Get most recent transactions (default behavior)
|
||||||
|
const recentTransactions = await account.getTransactions();
|
||||||
|
console.log(`Got ${recentTransactions.length} recent transactions`);
|
||||||
|
|
||||||
|
// Example 2: Get transactions with custom count
|
||||||
|
const smallBatch = await account.getTransactions({ count: 10 });
|
||||||
|
console.log(`Got ${smallBatch.length} transactions with custom count`);
|
||||||
|
|
||||||
|
// Example 3: Get older transactions using older_id
|
||||||
|
if (recentTransactions.length > 0) {
|
||||||
|
const oldestTransaction = recentTransactions[recentTransactions.length - 1];
|
||||||
|
const olderTransactions = await account.getTransactions({
|
||||||
|
count: 50,
|
||||||
|
older_id: oldestTransaction.id
|
||||||
|
});
|
||||||
|
console.log(`Got ${olderTransactions.length} older transactions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Get newer transactions using newer_id
|
||||||
|
if (recentTransactions.length > 0) {
|
||||||
|
const newestTransaction = recentTransactions[0];
|
||||||
|
const newerTransactions = await account.getTransactions({
|
||||||
|
count: 20,
|
||||||
|
newer_id: newestTransaction.id
|
||||||
|
});
|
||||||
|
console.log(`Got ${newerTransactions.length} newer transactions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 5: Backward compatibility - using number as newer_id
|
||||||
|
const backwardCompatible = await account.getTransactions(12345);
|
||||||
|
console.log(`Backward compatible call returned ${backwardCompatible.length} transactions`);
|
||||||
|
|
||||||
|
// Example 6: Paginating through all historical transactions
|
||||||
|
async function getAllTransactions(account: any): Promise<any[]> {
|
||||||
|
const allTransactions: any[] = [];
|
||||||
|
let lastTransactionId: number | false = false;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const options: IBunqPaginationOptions = {
|
||||||
|
count: 200,
|
||||||
|
older_id: lastTransactionId
|
||||||
|
};
|
||||||
|
|
||||||
|
const batch = await account.getTransactions(options);
|
||||||
|
|
||||||
|
if (batch.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
} else {
|
||||||
|
allTransactions.push(...batch);
|
||||||
|
lastTransactionId = batch[batch.length - 1].id;
|
||||||
|
console.log(`Fetched ${batch.length} transactions, total: ${allTransactions.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 7: Getting transactions between two dates
|
||||||
|
async function getTransactionsBetweenDates(
|
||||||
|
account: any,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date
|
||||||
|
): Promise<any[]> {
|
||||||
|
const transactions: any[] = [];
|
||||||
|
let olderId: number | false = false;
|
||||||
|
let keepFetching = true;
|
||||||
|
|
||||||
|
while (keepFetching) {
|
||||||
|
const batch = await account.getTransactions({
|
||||||
|
count: 200,
|
||||||
|
older_id: olderId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (batch.length === 0) {
|
||||||
|
keepFetching = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const transaction of batch) {
|
||||||
|
const transactionDate = new Date(transaction.created);
|
||||||
|
|
||||||
|
if (transactionDate >= startDate && transactionDate <= endDate) {
|
||||||
|
transactions.push(transaction);
|
||||||
|
} else if (transactionDate < startDate) {
|
||||||
|
// We've gone past our date range
|
||||||
|
keepFetching = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
olderId = batch[batch.length - 1].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const lastMonth = new Date();
|
||||||
|
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
||||||
|
const transactionsLastMonth = await getTransactionsBetweenDates(
|
||||||
|
account,
|
||||||
|
lastMonth,
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
console.log(`Found ${transactionsLastMonth.length} transactions in the last month`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the demo
|
||||||
|
demonstratePagination().catch(console.error);
|
172
example.stateless.ts
Normal file
172
example.stateless.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import * as bunq from './ts/index.js';
|
||||||
|
|
||||||
|
// Example of stateless usage of the bunq library
|
||||||
|
|
||||||
|
// 1. Initial session creation
|
||||||
|
async function createNewSession() {
|
||||||
|
const bunqAccount = new bunq.BunqAccount({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
deviceName: 'my-app',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize and get session data
|
||||||
|
const sessionData = await bunqAccount.init();
|
||||||
|
|
||||||
|
// Save session data to your preferred storage (database, file, etc.)
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
|
||||||
|
// Use the account
|
||||||
|
const { accounts } = await bunqAccount.getAccounts();
|
||||||
|
console.log('Found accounts:', accounts.length);
|
||||||
|
|
||||||
|
return sessionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Reusing an existing session
|
||||||
|
async function reuseExistingSession() {
|
||||||
|
// Load session data from your storage
|
||||||
|
const sessionData = await loadSessionFromDatabase();
|
||||||
|
|
||||||
|
const bunqAccount = new bunq.BunqAccount({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
deviceName: 'my-app',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with existing session
|
||||||
|
await bunqAccount.initWithSession(sessionData);
|
||||||
|
|
||||||
|
// Use the account - session refresh happens automatically
|
||||||
|
const { accounts, sessionData: updatedSession } = await bunqAccount.getAccounts();
|
||||||
|
|
||||||
|
// If session was refreshed, save the updated session data
|
||||||
|
if (updatedSession) {
|
||||||
|
await saveSessionToDatabase(updatedSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. OAuth token with existing installation
|
||||||
|
async function oauthWithExistingInstallation() {
|
||||||
|
const bunqAccount = new bunq.BunqAccount({
|
||||||
|
apiKey: 'oauth-access-token',
|
||||||
|
deviceName: 'my-oauth-app',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
isOAuthToken: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try normal initialization
|
||||||
|
const sessionData = await bunqAccount.init();
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
} catch (error) {
|
||||||
|
// If OAuth token already has installation, use existing
|
||||||
|
const existingInstallation = await loadInstallationFromDatabase();
|
||||||
|
const sessionData = await bunqAccount.initOAuthWithExistingInstallation(existingInstallation);
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Session validation
|
||||||
|
async function validateAndRefreshSession() {
|
||||||
|
const sessionData = await loadSessionFromDatabase();
|
||||||
|
|
||||||
|
const bunqAccount = new bunq.BunqAccount({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
deviceName: 'my-app',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bunqAccount.initWithSession(sessionData);
|
||||||
|
|
||||||
|
if (!bunqAccount.isSessionValid()) {
|
||||||
|
// Session expired, create new one
|
||||||
|
const newSessionData = await bunqAccount.init();
|
||||||
|
await saveSessionToDatabase(newSessionData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Session invalid, create new one
|
||||||
|
const newSessionData = await bunqAccount.init();
|
||||||
|
await saveSessionToDatabase(newSessionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Complete example with error handling
|
||||||
|
async function completeExample() {
|
||||||
|
let bunqAccount: bunq.BunqAccount;
|
||||||
|
let sessionData: bunq.ISessionData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to load existing session
|
||||||
|
const existingSession = await loadSessionFromDatabase();
|
||||||
|
|
||||||
|
bunqAccount = new bunq.BunqAccount({
|
||||||
|
apiKey: process.env.BUNQ_API_KEY!,
|
||||||
|
deviceName: 'my-production-app',
|
||||||
|
environment: 'PRODUCTION',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingSession) {
|
||||||
|
try {
|
||||||
|
await bunqAccount.initWithSession(existingSession);
|
||||||
|
console.log('Reused existing session');
|
||||||
|
} catch (error) {
|
||||||
|
// Session invalid, create new one
|
||||||
|
sessionData = await bunqAccount.init();
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
console.log('Created new session');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No existing session, create new one
|
||||||
|
sessionData = await bunqAccount.init();
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
console.log('Created new session');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the API
|
||||||
|
const { accounts, sessionData: updatedSession } = await bunqAccount.getAccounts();
|
||||||
|
|
||||||
|
// Save updated session if it was refreshed
|
||||||
|
if (updatedSession) {
|
||||||
|
await saveSessionToDatabase(updatedSession);
|
||||||
|
console.log('Session was refreshed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a payment
|
||||||
|
const account = accounts[0];
|
||||||
|
const payment = await bunq.BunqPayment.builder(bunqAccount, account)
|
||||||
|
.amount('10.00', 'EUR')
|
||||||
|
.toIban('NL91ABNA0417164300', 'Test Recipient')
|
||||||
|
.description('Test payment')
|
||||||
|
.create();
|
||||||
|
|
||||||
|
console.log('Payment created:', payment.id);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await bunqAccount.stop();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock storage functions (implement these with your actual storage)
|
||||||
|
async function saveSessionToDatabase(sessionData: bunq.ISessionData): Promise<void> {
|
||||||
|
// Implement your storage logic here
|
||||||
|
// Example: await db.sessions.save(sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSessionFromDatabase(): Promise<bunq.ISessionData | null> {
|
||||||
|
// Implement your storage logic here
|
||||||
|
// Example: return await db.sessions.findLatest();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInstallationFromDatabase(): Promise<Partial<bunq.ISessionData> | undefined> {
|
||||||
|
// Load just the installation data needed for OAuth
|
||||||
|
// Example: return await db.installations.findByApiKey();
|
||||||
|
return undefined;
|
||||||
|
}
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@apiclient.xyz/bunq",
|
"name": "@apiclient.xyz/bunq",
|
||||||
"version": "3.0.9",
|
"version": "4.0.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@apiclient.xyz/bunq",
|
"name": "@apiclient.xyz/bunq",
|
||||||
"version": "3.0.9",
|
"version": "4.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bunq-community/bunq-js-client": "^1.1.2",
|
"@bunq-community/bunq-js-client": "^1.1.2",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@apiclient.xyz/bunq",
|
"name": "@apiclient.xyz/bunq",
|
||||||
"version": "3.1.1",
|
"version": "4.1.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A full-featured TypeScript/JavaScript client for the bunq API",
|
"description": "A full-featured TypeScript/JavaScript client for the bunq API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
233
readme.md
233
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
|
- 🛡️ **Type Safety** - Compile-time type checking for all operations
|
||||||
- 📚 **Comprehensive Documentation** - Detailed examples for every feature
|
- 📚 **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
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -53,13 +72,21 @@ const bunq = new BunqAccount({
|
|||||||
environment: 'PRODUCTION' // or 'SANDBOX' for testing
|
environment: 'PRODUCTION' // or 'SANDBOX' for testing
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize connection
|
// Initialize connection and get session data
|
||||||
await bunq.init();
|
const sessionData = await bunq.init();
|
||||||
|
|
||||||
|
// IMPORTANT: Save the session data for reuse
|
||||||
|
await saveSessionToDatabase(sessionData);
|
||||||
|
|
||||||
// Get your accounts
|
// Get your accounts
|
||||||
const accounts = await bunq.getAccounts();
|
const { accounts, sessionData: updatedSession } = await bunq.getAccounts();
|
||||||
console.log(`Found ${accounts.length} accounts`);
|
console.log(`Found ${accounts.length} accounts`);
|
||||||
|
|
||||||
|
// If session was refreshed, save the updated data
|
||||||
|
if (updatedSession) {
|
||||||
|
await saveSessionToDatabase(updatedSession);
|
||||||
|
}
|
||||||
|
|
||||||
// Get recent transactions
|
// Get recent transactions
|
||||||
const transactions = await accounts[0].getTransactions();
|
const transactions = await accounts[0].getTransactions();
|
||||||
transactions.forEach(tx => {
|
transactions.forEach(tx => {
|
||||||
@@ -380,13 +407,73 @@ await new ExportBuilder(bunq, account)
|
|||||||
.downloadTo('/path/to/statement-with-attachments.pdf');
|
.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
|
```typescript
|
||||||
// Get user information
|
// Get user information
|
||||||
const user = await bunq.getUser();
|
const user = await bunq.getUser();
|
||||||
console.log(`Logged in as: ${user.displayName}`);
|
const userInfo = await user.getInfo();
|
||||||
console.log(`User type: ${user.type}`); // UserPerson, UserCompany, etc.
|
|
||||||
|
// 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
|
// Update user settings
|
||||||
await user.update({
|
await user.update({
|
||||||
@@ -395,21 +482,6 @@ await user.update({
|
|||||||
{ category: 'PAYMENT', notificationDeliveryMethod: 'PUSH' }
|
{ 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
|
## Advanced Usage
|
||||||
@@ -436,48 +508,34 @@ const bunq = new BunqAccount({
|
|||||||
apiKey: 'your-oauth-access-token', // OAuth token from bunq OAuth flow
|
apiKey: 'your-oauth-access-token', // OAuth token from bunq OAuth flow
|
||||||
deviceName: 'OAuth App',
|
deviceName: 'OAuth App',
|
||||||
environment: 'PRODUCTION',
|
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
|
// 1. They go through installation → device → session creation
|
||||||
// 2. The OAuth token is used as the 'secret' during authentication
|
// 2. The OAuth token is used as the 'secret' during authentication
|
||||||
// 3. A session token is created and used for all API calls
|
// 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"
|
|
||||||
|
|
||||||
// OAuth Session Caching (v3.0.9+)
|
|
||||||
// The library automatically caches OAuth sessions to prevent multiple authentication attempts
|
|
||||||
|
|
||||||
// Multiple instances with the same OAuth token will reuse the cached session
|
|
||||||
const bunq1 = new BunqAccount({
|
|
||||||
apiKey: 'your-oauth-access-token',
|
|
||||||
deviceName: 'OAuth App Instance 1',
|
|
||||||
environment: 'PRODUCTION',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const bunq2 = new BunqAccount({
|
|
||||||
apiKey: 'your-oauth-access-token', // Same token
|
|
||||||
deviceName: 'OAuth App Instance 2',
|
|
||||||
environment: 'PRODUCTION',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await bunq1.init(); // Creates new session
|
|
||||||
await bunq2.init(); // Reuses cached session from bunq1
|
|
||||||
|
|
||||||
// This prevents "Superfluous authentication" errors when multiple instances
|
|
||||||
// try to authenticate with the same OAuth token
|
|
||||||
|
|
||||||
// Cache management methods
|
|
||||||
BunqAccount.clearOAuthCache(); // Clear all cached OAuth sessions
|
|
||||||
BunqAccount.clearOAuthCacheForToken('token', 'PRODUCTION'); // Clear specific token
|
|
||||||
const cacheSize = BunqAccount.getOAuthCacheSize(); // Get current cache size
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
@@ -619,6 +677,67 @@ await bunqJSClient.registerSession();
|
|||||||
await bunq.init();
|
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
|
## Testing
|
||||||
|
|
||||||
The library includes comprehensive test coverage:
|
The library includes comprehensive test coverage:
|
||||||
|
@@ -1,105 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as bunq from '../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('should cache and reuse OAuth sessions', async () => {
|
|
||||||
// Create first OAuth account instance
|
|
||||||
const oauthBunq1 = new bunq.BunqAccount({
|
|
||||||
apiKey: 'test-oauth-token-cache',
|
|
||||||
deviceName: 'OAuth Test App 1',
|
|
||||||
environment: 'SANDBOX',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create second OAuth account instance with same token
|
|
||||||
const oauthBunq2 = new bunq.BunqAccount({
|
|
||||||
apiKey: 'test-oauth-token-cache',
|
|
||||||
deviceName: 'OAuth Test App 2',
|
|
||||||
environment: 'SANDBOX',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize first instance
|
|
||||||
await oauthBunq1.init();
|
|
||||||
console.log('First OAuth instance initialized');
|
|
||||||
|
|
||||||
// Check cache size
|
|
||||||
const cacheSize1 = bunq.BunqAccount.getOAuthCacheSize();
|
|
||||||
console.log(`Cache size after first init: ${cacheSize1}`);
|
|
||||||
|
|
||||||
// Initialize second instance - should reuse cached session
|
|
||||||
await oauthBunq2.init();
|
|
||||||
console.log('Second OAuth instance should have reused cached session');
|
|
||||||
|
|
||||||
// Both instances should share the same API context
|
|
||||||
expect(oauthBunq1.apiContext).toEqual(oauthBunq2.apiContext);
|
|
||||||
|
|
||||||
// Cache size should still be 1
|
|
||||||
const cacheSize2 = bunq.BunqAccount.getOAuthCacheSize();
|
|
||||||
expect(cacheSize2).toEqual(1);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail with invalid token, but we can test the caching logic
|
|
||||||
console.log('OAuth caching test completed (expected auth failure with mock token)');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should handle OAuth session cache clearing', async () => {
|
|
||||||
// Create OAuth account instance
|
|
||||||
const oauthBunq = new bunq.BunqAccount({
|
|
||||||
apiKey: 'test-oauth-token-clear',
|
|
||||||
deviceName: 'OAuth Test App',
|
|
||||||
environment: 'SANDBOX',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await oauthBunq.init();
|
|
||||||
} catch (error) {
|
|
||||||
// Expected failure with mock token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear specific token from cache
|
|
||||||
bunq.BunqAccount.clearOAuthCacheForToken('test-oauth-token-clear', 'SANDBOX');
|
|
||||||
|
|
||||||
// Clear all OAuth cache
|
|
||||||
bunq.BunqAccount.clearOAuthCache();
|
|
||||||
|
|
||||||
// Cache should be empty
|
|
||||||
const cacheSize = bunq.BunqAccount.getOAuthCacheSize();
|
|
||||||
expect(cacheSize).toEqual(0);
|
|
||||||
|
|
||||||
console.log('OAuth cache clearing test passed');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should handle different OAuth tokens separately', async () => {
|
|
||||||
const oauthBunq1 = new bunq.BunqAccount({
|
|
||||||
apiKey: 'test-oauth-token-1',
|
|
||||||
deviceName: 'OAuth Test App 1',
|
|
||||||
environment: 'SANDBOX',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const oauthBunq2 = new bunq.BunqAccount({
|
|
||||||
apiKey: 'test-oauth-token-2',
|
|
||||||
deviceName: 'OAuth Test App 2',
|
|
||||||
environment: 'SANDBOX',
|
|
||||||
isOAuthToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await oauthBunq1.init();
|
|
||||||
await oauthBunq2.init();
|
|
||||||
} catch (error) {
|
|
||||||
// Expected failures with mock tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have 2 different cached sessions
|
|
||||||
const cacheSize = bunq.BunqAccount.getOAuthCacheSize();
|
|
||||||
console.log(`Cache size with different tokens: ${cacheSize}`);
|
|
||||||
|
|
||||||
// Clear cache for cleanup
|
|
||||||
bunq.BunqAccount.clearOAuthCache();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
@@ -3,7 +3,7 @@ import { BunqApiContext } from './bunq.classes.apicontext.js';
|
|||||||
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
||||||
import { BunqUser } from './bunq.classes.user.js';
|
import { BunqUser } from './bunq.classes.user.js';
|
||||||
import { BunqApiError } from './bunq.classes.httpclient.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 {
|
export interface IBunqConstructorOptions {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
@@ -17,9 +17,6 @@ export interface IBunqConstructorOptions {
|
|||||||
* the main bunq account
|
* the main bunq account
|
||||||
*/
|
*/
|
||||||
export class BunqAccount {
|
export class BunqAccount {
|
||||||
// Static cache for OAuth token sessions to prevent multiple authentication attempts
|
|
||||||
private static oauthSessionCache = new Map<string, BunqApiContext>();
|
|
||||||
|
|
||||||
public options: IBunqConstructorOptions;
|
public options: IBunqConstructorOptions;
|
||||||
public apiContext: BunqApiContext;
|
public apiContext: BunqApiContext;
|
||||||
public userId: number;
|
public userId: number;
|
||||||
@@ -33,61 +30,37 @@ export class BunqAccount {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the bunq account
|
* Initialize the bunq account
|
||||||
|
* @returns The session data that can be persisted by the consumer
|
||||||
*/
|
*/
|
||||||
public async init() {
|
public async init(): Promise<ISessionData> {
|
||||||
// For OAuth tokens, check if we already have a cached session
|
// Create API context for both OAuth tokens and regular API keys
|
||||||
if (this.options.isOAuthToken) {
|
this.apiContext = new BunqApiContext({
|
||||||
const cacheKey = `${this.options.apiKey}_${this.options.environment}`;
|
apiKey: this.options.apiKey,
|
||||||
const cachedContext = BunqAccount.oauthSessionCache.get(cacheKey);
|
environment: this.options.environment,
|
||||||
|
deviceDescription: this.options.deviceName,
|
||||||
if (cachedContext && cachedContext.hasValidSession()) {
|
permittedIps: this.options.permittedIps,
|
||||||
// Reuse existing session
|
isOAuthToken: this.options.isOAuthToken
|
||||||
this.apiContext = cachedContext;
|
});
|
||||||
console.log('Reusing existing OAuth session from cache');
|
|
||||||
} else {
|
|
||||||
// Create new context and cache it
|
|
||||||
this.apiContext = new BunqApiContext({
|
|
||||||
apiKey: this.options.apiKey,
|
|
||||||
environment: this.options.environment,
|
|
||||||
deviceDescription: this.options.deviceName,
|
|
||||||
permittedIps: this.options.permittedIps,
|
|
||||||
isOAuthToken: this.options.isOAuthToken
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.apiContext.init();
|
|
||||||
// Cache the successfully initialized context
|
|
||||||
BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle "Superfluous authentication" or "Authentication token already has a user session" errors
|
|
||||||
if (error instanceof BunqApiError) {
|
|
||||||
const errorMessages = error.errors.map(e => e.error_description).join(' ');
|
|
||||||
if (errorMessages.includes('Superfluous authentication') ||
|
|
||||||
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();
|
|
||||||
// Cache the context with new session
|
|
||||||
BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular API key flow
|
|
||||||
this.apiContext = new BunqApiContext({
|
|
||||||
apiKey: this.options.apiKey,
|
|
||||||
environment: this.options.environment,
|
|
||||||
deviceDescription: this.options.deviceName,
|
|
||||||
permittedIps: this.options.permittedIps,
|
|
||||||
isOAuthToken: this.options.isOAuthToken
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.apiContext.init();
|
let sessionData: ISessionData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
const errorMessages = error.errors.map(e => e.error_description).join(' ');
|
||||||
|
if (errorMessages.includes('Superfluous authentication') ||
|
||||||
|
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
|
||||||
|
sessionData = await this.apiContext.initWithExistingInstallation();
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user instance
|
// Create user instance
|
||||||
@@ -95,6 +68,27 @@ export class BunqAccount {
|
|||||||
|
|
||||||
// Get user info
|
// Get user info
|
||||||
await this.getUserInfo();
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,9 +113,10 @@ export class BunqAccount {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all monetary accounts
|
* Get all monetary accounts
|
||||||
|
* @returns An array of monetary accounts and updated session data if session was refreshed
|
||||||
*/
|
*/
|
||||||
public async getAccounts(): Promise<BunqMonetaryAccount[]> {
|
public async getAccounts(): Promise<{ accounts: BunqMonetaryAccount[], sessionData?: ISessionData }> {
|
||||||
await this.apiContext.ensureValidSession();
|
const sessionData = await this.apiContext.ensureValidSession();
|
||||||
|
|
||||||
const response = await this.apiContext.getHttpClient().list(
|
const response = await this.apiContext.getHttpClient().list(
|
||||||
`/v1/user/${this.userId}/monetary-account`
|
`/v1/user/${this.userId}/monetary-account`
|
||||||
@@ -135,21 +130,23 @@ export class BunqAccount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return accountsArray;
|
return { accounts: accountsArray, sessionData: sessionData || undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific monetary account
|
* Get a specific monetary account
|
||||||
|
* @returns The monetary account and updated session data if session was refreshed
|
||||||
*/
|
*/
|
||||||
public async getAccount(accountId: number): Promise<BunqMonetaryAccount> {
|
public async getAccount(accountId: number): Promise<{ account: BunqMonetaryAccount, sessionData?: ISessionData }> {
|
||||||
await this.apiContext.ensureValidSession();
|
const sessionData = await this.apiContext.ensureValidSession();
|
||||||
|
|
||||||
const response = await this.apiContext.getHttpClient().get(
|
const response = await this.apiContext.getHttpClient().get(
|
||||||
`/v1/user/${this.userId}/monetary-account/${accountId}`
|
`/v1/user/${this.userId}/monetary-account/${accountId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.Response && response.Response[0]) {
|
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');
|
throw new Error('Account not found');
|
||||||
@@ -198,6 +195,54 @@ export class BunqAccount {
|
|||||||
return this.apiContext.getHttpClient();
|
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
|
* Stop the bunq account and clean up
|
||||||
*/
|
*/
|
||||||
@@ -207,28 +252,4 @@ export class BunqAccount {
|
|||||||
this.apiContext = null;
|
this.apiContext = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the OAuth session cache
|
|
||||||
*/
|
|
||||||
public static clearOAuthCache(): void {
|
|
||||||
BunqAccount.oauthSessionCache.clear();
|
|
||||||
console.log('OAuth session cache cleared');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear a specific OAuth token from the cache
|
|
||||||
*/
|
|
||||||
public static clearOAuthCacheForToken(apiKey: string, environment: 'SANDBOX' | 'PRODUCTION'): void {
|
|
||||||
const cacheKey = `${apiKey}_${environment}`;
|
|
||||||
BunqAccount.oauthSessionCache.delete(cacheKey);
|
|
||||||
console.log(`OAuth session cache cleared for token in ${environment} environment`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current size of the OAuth cache
|
|
||||||
*/
|
|
||||||
public static getOAuthCacheSize(): number {
|
|
||||||
return BunqAccount.oauthSessionCache.size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import * as plugins from './bunq.plugins.js';
|
import * as plugins from './bunq.plugins.js';
|
||||||
import * as paths from './bunq.paths.js';
|
|
||||||
import { BunqCrypto } from './bunq.classes.crypto.js';
|
import { BunqCrypto } from './bunq.classes.crypto.js';
|
||||||
import { BunqSession } from './bunq.classes.session.js';
|
import { BunqSession } from './bunq.classes.session.js';
|
||||||
import type { IBunqApiContext } from './bunq.interfaces.js';
|
import type { IBunqApiContext, ISessionData } from './bunq.interfaces.js';
|
||||||
|
|
||||||
export interface IBunqApiContextOptions {
|
export interface IBunqApiContextOptions {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -17,7 +16,6 @@ export class BunqApiContext {
|
|||||||
private crypto: BunqCrypto;
|
private crypto: BunqCrypto;
|
||||||
private session: BunqSession;
|
private session: BunqSession;
|
||||||
private context: IBunqApiContext;
|
private context: IBunqApiContext;
|
||||||
private contextFilePath: string;
|
|
||||||
|
|
||||||
constructor(options: IBunqApiContextOptions) {
|
constructor(options: IBunqApiContextOptions) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@@ -32,38 +30,14 @@ export class BunqApiContext {
|
|||||||
: 'https://public-api.sandbox.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);
|
this.session = new BunqSession(this.crypto, this.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the API context (installation, device, session)
|
* Initialize the API context (installation, device, session)
|
||||||
|
* @returns The session data that can be persisted by the consumer
|
||||||
*/
|
*/
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<ISessionData> {
|
||||||
// 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
|
// Create new session
|
||||||
await this.session.init(
|
await this.session.init(
|
||||||
this.options.deviceDescription,
|
this.options.deviceDescription,
|
||||||
@@ -75,42 +49,96 @@ export class BunqApiContext {
|
|||||||
this.session.setOAuthMode(true);
|
this.session.setOAuthMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save context
|
return this.exportSession();
|
||||||
await this.saveContext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the current context to file
|
* Initialize the API context with existing session data
|
||||||
|
* @param sessionData The session data to restore
|
||||||
*/
|
*/
|
||||||
private async saveContext(): Promise<void> {
|
public async initWithSession(sessionData: ISessionData): Promise<void> {
|
||||||
await plugins.smartfile.fs.ensureDir(paths.nogitDir);
|
// Validate session data
|
||||||
|
if (!sessionData.sessionToken || !sessionData.sessionId) {
|
||||||
const contextToSave = {
|
throw new Error('Invalid session data: missing session token or ID');
|
||||||
...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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore crypto keys
|
||||||
|
this.crypto.setKeys(
|
||||||
|
sessionData.clientPrivateKey,
|
||||||
|
sessionData.clientPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the current session data for persistence
|
||||||
|
* @returns The session data that can be saved by the consumer
|
||||||
|
*/
|
||||||
|
public exportSession(): ISessionData {
|
||||||
|
const context = this.session.getContext();
|
||||||
|
|
||||||
|
if (!context.sessionToken || !context.sessionId) {
|
||||||
|
throw new Error('No active session to export');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,24 +157,24 @@ export class BunqApiContext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh session if needed
|
* Refresh session if needed
|
||||||
|
* @returns Updated session data if session was refreshed, null otherwise
|
||||||
*/
|
*/
|
||||||
public async ensureValidSession(): Promise<void> {
|
public async ensureValidSession(): Promise<ISessionData | null> {
|
||||||
|
const wasValid = this.session.isSessionValid();
|
||||||
await this.session.refreshSession();
|
await this.session.refreshSession();
|
||||||
await this.saveContext();
|
|
||||||
|
// Return updated session data only if session was actually refreshed
|
||||||
|
if (!wasValid) {
|
||||||
|
return this.exportSession();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the current session and clean up
|
* Destroy the current session
|
||||||
*/
|
*/
|
||||||
public async destroy(): Promise<void> {
|
public async destroy(): Promise<void> {
|
||||||
await this.session.destroySession();
|
await this.session.destroySession();
|
||||||
|
|
||||||
// Remove saved context
|
|
||||||
try {
|
|
||||||
await plugins.smartfile.fs.remove(this.contextFilePath);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors when removing file
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,24 +200,27 @@ export class BunqApiContext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize with existing installation and device (for OAuth tokens that already completed these steps)
|
* Initialize with existing installation and device (for OAuth tokens that already completed these steps)
|
||||||
|
* @param existingInstallation Optional partial session data with just installation/device info
|
||||||
|
* @returns The new session data
|
||||||
*/
|
*/
|
||||||
public async initWithExistingInstallation(): Promise<void> {
|
public async initWithExistingInstallation(existingInstallation?: Partial<ISessionData>): Promise<ISessionData> {
|
||||||
// For OAuth tokens that already have installation/device but need a new session
|
// For OAuth tokens that already have installation/device but need a new session
|
||||||
// We need to:
|
|
||||||
// 1. Try to load existing installation/device info
|
|
||||||
// 2. Create a new session using the OAuth token as the secret
|
|
||||||
|
|
||||||
const existingContext = await this.loadContext();
|
if (existingInstallation && existingInstallation.clientPrivateKey && existingInstallation.clientPublicKey) {
|
||||||
|
|
||||||
if (existingContext && existingContext.clientPrivateKey && existingContext.clientPublicKey) {
|
|
||||||
// Restore crypto keys from previous installation
|
// Restore crypto keys from previous installation
|
||||||
this.crypto.setKeys(
|
this.crypto.setKeys(
|
||||||
existingContext.clientPrivateKey,
|
existingInstallation.clientPrivateKey,
|
||||||
existingContext.clientPublicKey
|
existingInstallation.clientPublicKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update context with existing installation data
|
// Update context with existing installation data
|
||||||
this.context = { ...this.context, ...existingContext };
|
this.context = {
|
||||||
|
...this.context,
|
||||||
|
installationToken: existingInstallation.installationToken,
|
||||||
|
serverPublicKey: existingInstallation.serverPublicKey,
|
||||||
|
clientPrivateKey: existingInstallation.clientPrivateKey,
|
||||||
|
clientPublicKey: existingInstallation.clientPublicKey
|
||||||
|
};
|
||||||
|
|
||||||
// Create new session instance
|
// Create new session instance
|
||||||
this.session = new BunqSession(this.crypto, this.context);
|
this.session = new BunqSession(this.crypto, this.context);
|
||||||
@@ -206,14 +237,14 @@ export class BunqApiContext {
|
|||||||
this.session.setOAuthMode(true);
|
this.session.setOAuthMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveContext();
|
|
||||||
console.log('Successfully created new session with existing installation');
|
console.log('Successfully created new session with existing installation');
|
||||||
|
return this.exportSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to create session with OAuth token: ${error.message}`);
|
throw new Error(`Failed to create session with OAuth token: ${error.message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No existing installation, fall back to full init
|
// No existing installation, fall back to full init
|
||||||
throw new Error('No existing installation found, full initialization required');
|
throw new Error('No existing installation provided, full initialization required');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -53,13 +53,13 @@ export class BunqHttpClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.params) {
|
if (options.params) {
|
||||||
const params = new URLSearchParams();
|
const queryParams: { [key: string]: string } = {};
|
||||||
Object.entries(options.params).forEach(([key, value]) => {
|
Object.entries(options.params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
params.append(key, String(value));
|
queryParams[key] = String(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
requestOptions.queryParams = params.toString();
|
requestOptions.queryParams = queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -111,18 +111,41 @@ export class BunqMonetaryAccount {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* gets all transactions on this account
|
* gets all transactions on this account
|
||||||
|
* @param options - Pagination options or a number for backward compatibility (treated as newer_id)
|
||||||
*/
|
*/
|
||||||
public async getTransactions(startingIdArg: number | false = false): Promise<BunqTransaction[]> {
|
public async getTransactions(options?: IBunqPaginationOptions | number | false): Promise<BunqTransaction[]> {
|
||||||
const paginationOptions: IBunqPaginationOptions = {
|
let paginationOptions: IBunqPaginationOptions = {};
|
||||||
count: 200,
|
|
||||||
newer_id: startingIdArg,
|
// Backward compatibility: if a number or false is passed, treat it as newer_id
|
||||||
|
if (typeof options === 'number' || options === false) {
|
||||||
|
paginationOptions.newer_id = options;
|
||||||
|
} else if (options) {
|
||||||
|
paginationOptions = { ...options };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default count if not specified
|
||||||
|
if (!paginationOptions.count) {
|
||||||
|
paginationOptions.count = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build clean pagination object - only include properties that are not false/undefined
|
||||||
|
const cleanPaginationOptions: IBunqPaginationOptions = {
|
||||||
|
count: paginationOptions.count,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (paginationOptions.newer_id !== undefined && paginationOptions.newer_id !== false) {
|
||||||
|
cleanPaginationOptions.newer_id = paginationOptions.newer_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paginationOptions.older_id !== undefined && paginationOptions.older_id !== false) {
|
||||||
|
cleanPaginationOptions.older_id = paginationOptions.older_id;
|
||||||
|
}
|
||||||
|
|
||||||
await this.bunqAccountRef.apiContext.ensureValidSession();
|
await this.bunqAccountRef.apiContext.ensureValidSession();
|
||||||
|
|
||||||
const response = await this.bunqAccountRef.getHttpClient().list(
|
const response = await this.bunqAccountRef.getHttpClient().list(
|
||||||
`/v1/user/${this.bunqAccountRef.userId}/monetary-account/${this.id}/payment`,
|
`/v1/user/${this.bunqAccountRef.userId}/monetary-account/${this.id}/payment`,
|
||||||
paginationOptions
|
cleanPaginationOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionsArray: BunqTransaction[] = [];
|
const transactionsArray: BunqTransaction[] = [];
|
||||||
|
@@ -109,11 +109,15 @@ export class BunqSession {
|
|||||||
secret: this.context.apiKey
|
secret: this.context.apiKey
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract session token and user info
|
// Extract session token, session ID, and user info
|
||||||
let sessionToken: string;
|
let sessionToken: string;
|
||||||
|
let sessionId: number;
|
||||||
let userId: number;
|
let userId: number;
|
||||||
|
|
||||||
for (const item of response.Response) {
|
for (const item of response.Response) {
|
||||||
|
if (item.Id) {
|
||||||
|
sessionId = item.Id.id;
|
||||||
|
}
|
||||||
if (item.Token) {
|
if (item.Token) {
|
||||||
sessionToken = item.Token.token;
|
sessionToken = item.Token.token;
|
||||||
}
|
}
|
||||||
@@ -126,12 +130,13 @@ export class BunqSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionToken || !userId) {
|
if (!sessionToken || !userId || !sessionId) {
|
||||||
throw new Error('Failed to create session');
|
throw new Error('Failed to create session');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update context
|
// Update context
|
||||||
this.context.sessionToken = sessionToken;
|
this.context.sessionToken = sessionToken;
|
||||||
|
this.context.sessionId = sessionId;
|
||||||
|
|
||||||
// Update HTTP client context
|
// Update HTTP client context
|
||||||
this.httpClient.updateContext({
|
this.httpClient.updateContext({
|
||||||
@@ -140,6 +145,7 @@ export class BunqSession {
|
|||||||
|
|
||||||
// Set session expiry (bunq sessions expire after 10 minutes of inactivity)
|
// Set session expiry (bunq sessions expire after 10 minutes of inactivity)
|
||||||
this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 600000);
|
this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 600000);
|
||||||
|
this.context.expiresAt = new Date(Date.now() + 600000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +156,9 @@ export class BunqSession {
|
|||||||
if (isOAuth) {
|
if (isOAuth) {
|
||||||
// OAuth tokens don't expire in the same way as regular sessions
|
// OAuth tokens don't expire in the same way as regular sessions
|
||||||
// Set a far future expiry time
|
// Set a far future expiry time
|
||||||
this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 365 * 24 * 60 * 60 * 1000);
|
const farFutureTime = Date.now() + 365 * 24 * 60 * 60 * 1000;
|
||||||
|
this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(farFutureTime);
|
||||||
|
this.context.expiresAt = new Date(farFutureTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +200,13 @@ export class BunqSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current session ID from the token
|
* Get the current session ID
|
||||||
*/
|
*/
|
||||||
private getSessionId(): string {
|
private getSessionId(): string {
|
||||||
// In a real implementation, we would need to store the session ID
|
if (!this.context.sessionId) {
|
||||||
// For now, return a placeholder
|
throw new Error('Session ID not available');
|
||||||
return '0';
|
}
|
||||||
|
return this.context.sessionId.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,9 +4,23 @@ export interface IBunqApiContext {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
installationToken?: string;
|
installationToken?: string;
|
||||||
sessionToken?: string;
|
sessionToken?: string;
|
||||||
|
sessionId?: number;
|
||||||
serverPublicKey?: string;
|
serverPublicKey?: string;
|
||||||
clientPrivateKey?: string;
|
clientPrivateKey?: string;
|
||||||
clientPublicKey?: string;
|
clientPublicKey?: string;
|
||||||
|
expiresAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISessionData {
|
||||||
|
sessionId: number;
|
||||||
|
sessionToken: string;
|
||||||
|
installationToken: string;
|
||||||
|
serverPublicKey: string;
|
||||||
|
clientPrivateKey: string;
|
||||||
|
clientPublicKey: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
environment: 'SANDBOX' | 'PRODUCTION';
|
||||||
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBunqError {
|
export interface IBunqError {
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import * as plugins from './bunq.plugins.js';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(__dirname, '../');
|
|
||||||
export const nogitDir = plugins.path.join(packageDir, './.nogit/');
|
|
||||||
|
|
||||||
export const bunqJsonProductionFile = plugins.path.join(nogitDir, 'bunqproduction.json');
|
|
||||||
export const bunqJsonSandboxFile = plugins.path.join(nogitDir, 'bunqsandbox.json');
|
|
Reference in New Issue
Block a user