Files
michaelswanson 860d5f55cc Initial commit
2026-06-25 19:58:40 +00:00

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;
}