Initial commit
This commit is contained in:
Executable
+183
@@ -0,0 +1,183 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const API_BASE = 'https://api.enablebanking.com';
|
||||
|
||||
function base64url(input) {
|
||||
return Buffer.from(input)
|
||||
.toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
export function publicEnableBankingConfig(config) {
|
||||
return {
|
||||
...config,
|
||||
private_key_pem: undefined,
|
||||
has_private_key: Boolean(config.private_key_pem?.trim()),
|
||||
transaction_categories: config.transaction_categories || {},
|
||||
accounts: (config.accounts || []).map(account => ({
|
||||
...account,
|
||||
display_name: account.alias || account.name || 'Konto',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function createEnableBankingJwt({ application_id, private_key_pem, expiresInSeconds = 3600 }) {
|
||||
if (!application_id?.trim()) throw new Error('Enable Banking application_id saknas');
|
||||
if (!private_key_pem?.trim()) throw new Error('Enable Banking private key saknas');
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const header = {
|
||||
typ: 'JWT',
|
||||
alg: 'RS256',
|
||||
kid: application_id.trim(),
|
||||
};
|
||||
const payload = {
|
||||
iss: 'enablebanking.com',
|
||||
aud: 'api.enablebanking.com',
|
||||
iat,
|
||||
exp: iat + expiresInSeconds,
|
||||
};
|
||||
|
||||
const unsignedToken = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
|
||||
const signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(unsignedToken);
|
||||
signer.end();
|
||||
const signature = signer.sign(private_key_pem.trim());
|
||||
|
||||
return `${unsignedToken}.${base64url(signature)}`;
|
||||
}
|
||||
|
||||
export async function enableBankingRequest(config, path, { method = 'GET', body } = {}) {
|
||||
const token = createEnableBankingJwt(config);
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let data = null;
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = typeof data === 'object' && data?.message
|
||||
? data.message
|
||||
: typeof data === 'object' && data?.error
|
||||
? data.error
|
||||
: text || `Enable Banking API error ${response.status}`;
|
||||
const error = new Error(`${message} [${method} ${path}]`);
|
||||
error.status = response.status;
|
||||
error.details = data;
|
||||
error.path = path;
|
||||
error.method = method;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchAspsps(config, country) {
|
||||
return enableBankingRequest(config, `/aspsps?country=${encodeURIComponent(country)}`);
|
||||
}
|
||||
|
||||
export async function startAuthorization(config, body) {
|
||||
return enableBankingRequest(config, '/auth', {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSession(config, code) {
|
||||
return enableBankingRequest(config, '/sessions', {
|
||||
method: 'POST',
|
||||
body: { code },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSession(config, sessionId) {
|
||||
return enableBankingRequest(config, `/sessions/${sessionId}`);
|
||||
}
|
||||
|
||||
export async function deleteSession(config, sessionId) {
|
||||
return enableBankingRequest(config, `/sessions/${sessionId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function getAccountDetails(config, accountId) {
|
||||
return enableBankingRequest(config, `/accounts/${accountId}/details`);
|
||||
}
|
||||
|
||||
export async function getAccountBalances(config, accountId) {
|
||||
return enableBankingRequest(config, `/accounts/${accountId}/balances`);
|
||||
}
|
||||
|
||||
export async function getAllTransactions(config, accountId, options = {}) {
|
||||
const allTransactions = [];
|
||||
let continuationKey = null;
|
||||
|
||||
do {
|
||||
const variants = [
|
||||
{ includeDateFrom: true, includeDateTo: true, includeStrategy: true },
|
||||
{ includeDateFrom: true, includeDateTo: false, includeStrategy: true },
|
||||
{ includeDateFrom: true, includeDateTo: false, includeStrategy: false },
|
||||
{ includeDateFrom: false, includeDateTo: false, includeStrategy: false },
|
||||
];
|
||||
|
||||
let page = null;
|
||||
let lastError = null;
|
||||
|
||||
for (const variant of variants) {
|
||||
const params = new URLSearchParams();
|
||||
if (variant.includeDateFrom && options.date_from) params.set('date_from', options.date_from);
|
||||
if (variant.includeDateTo && options.date_to) params.set('date_to', options.date_to);
|
||||
if (continuationKey) params.set('continuation_key', continuationKey);
|
||||
if (variant.includeStrategy) params.set('strategy', 'all');
|
||||
|
||||
const query = params.toString();
|
||||
|
||||
try {
|
||||
page = await enableBankingRequest(
|
||||
config,
|
||||
`/accounts/${accountId}/transactions${query ? `?${query}` : ''}`
|
||||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const badParams = (error.status === 400 || error.status === 422)
|
||||
&& String(error.message || '').toLowerCase().includes('wrong request parameters provided');
|
||||
if (!badParams) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
throw lastError || new Error('Kunde inte hämta transaktioner');
|
||||
}
|
||||
|
||||
const transactionGroups = Array.isArray(page?.transactions)
|
||||
? page.transactions
|
||||
: [
|
||||
...((page?.transactions?.booked ?? []).map(transaction => ({ ...transaction, status: transaction.status || 'booked' }))),
|
||||
...((page?.transactions?.pending ?? []).map(transaction => ({ ...transaction, status: transaction.status || 'pending' }))),
|
||||
...((page?.booked ?? []).map(transaction => ({ ...transaction, status: transaction.status || 'booked' }))),
|
||||
...((page?.pending ?? []).map(transaction => ({ ...transaction, status: transaction.status || 'pending' }))),
|
||||
];
|
||||
|
||||
allTransactions.push(...transactionGroups);
|
||||
continuationKey = page?.continuation_key || null;
|
||||
} while (continuationKey);
|
||||
|
||||
return allTransactions;
|
||||
}
|
||||
Reference in New Issue
Block a user