184 lines
5.7 KiB
JavaScript
Executable File
184 lines
5.7 KiB
JavaScript
Executable File
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;
|
|
}
|