@ktmcp-cli/nordigen 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.js ADDED
@@ -0,0 +1,229 @@
1
+ import axios from 'axios';
2
+ import { getConfig, setConfig, hasValidToken } from './config.js';
3
+
4
+ const NORDIGEN_BASE_URL = 'https://ob.nordigen.com/api/v2';
5
+
6
+ /**
7
+ * Obtain a new JWT access token
8
+ */
9
+ async function obtainAccessToken() {
10
+ const secretId = getConfig('secretId');
11
+ const secretKey = getConfig('secretKey');
12
+
13
+ if (!secretId || !secretKey) {
14
+ throw new Error('Credentials not configured. Run: nordigencom config set --secret-id <id> --secret-key <key>');
15
+ }
16
+
17
+ try {
18
+ const response = await axios.post(`${NORDIGEN_BASE_URL}/token/new/`, {
19
+ secret_id: secretId,
20
+ secret_key: secretKey
21
+ });
22
+
23
+ const { access, refresh, access_expires } = response.data;
24
+ setConfig('accessToken', access);
25
+ if (refresh) setConfig('refreshToken', refresh);
26
+ // access_expires is in seconds
27
+ setConfig('tokenExpiry', Date.now() + (access_expires * 1000));
28
+
29
+ return access;
30
+ } catch (error) {
31
+ const msg = error.response?.data?.summary || error.message;
32
+ throw new Error(`Token request failed: ${msg}`);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Refresh the access token using refresh token
38
+ */
39
+ async function refreshAccessToken() {
40
+ const refreshToken = getConfig('refreshToken');
41
+
42
+ if (!refreshToken) {
43
+ return await obtainAccessToken();
44
+ }
45
+
46
+ try {
47
+ const response = await axios.post(`${NORDIGEN_BASE_URL}/token/refresh/`, {
48
+ refresh: refreshToken
49
+ });
50
+
51
+ const { access, access_expires } = response.data;
52
+ setConfig('accessToken', access);
53
+ setConfig('tokenExpiry', Date.now() + (access_expires * 1000));
54
+
55
+ return access;
56
+ } catch (error) {
57
+ // If refresh fails, try to get a new token
58
+ return await obtainAccessToken();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get a valid access token
64
+ */
65
+ async function getAccessToken() {
66
+ if (hasValidToken()) {
67
+ return getConfig('accessToken');
68
+ }
69
+
70
+ return await refreshAccessToken();
71
+ }
72
+
73
+ /**
74
+ * Make an authenticated API request
75
+ */
76
+ async function apiRequest(method, endpoint, data = null, params = null) {
77
+ const token = await getAccessToken();
78
+
79
+ const config = {
80
+ method,
81
+ url: `${NORDIGEN_BASE_URL}${endpoint}`,
82
+ headers: {
83
+ 'Authorization': `Bearer ${token}`,
84
+ 'Accept': 'application/json',
85
+ 'Content-Type': 'application/json'
86
+ }
87
+ };
88
+
89
+ if (params) config.params = params;
90
+ if (data) config.data = data;
91
+
92
+ try {
93
+ const response = await axios(config);
94
+ return response.data;
95
+ } catch (error) {
96
+ handleApiError(error);
97
+ }
98
+ }
99
+
100
+ function handleApiError(error) {
101
+ if (error.response) {
102
+ const status = error.response.status;
103
+ const data = error.response.data;
104
+
105
+ if (status === 401) {
106
+ throw new Error('Authentication failed. Run: nordigencom auth login');
107
+ } else if (status === 403) {
108
+ throw new Error('Access forbidden. Check your API permissions.');
109
+ } else if (status === 404) {
110
+ throw new Error('Resource not found.');
111
+ } else if (status === 429) {
112
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
113
+ } else {
114
+ const message = data?.summary || data?.detail || data?.message || JSON.stringify(data);
115
+ throw new Error(`API Error (${status}): ${message}`);
116
+ }
117
+ } else if (error.request) {
118
+ throw new Error('No response from Nordigen API. Check your internet connection.');
119
+ } else {
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ // ============================================================
125
+ // INSTITUTIONS
126
+ // ============================================================
127
+
128
+ export async function listInstitutions({ country } = {}) {
129
+ const params = {};
130
+ if (country) params.country = country;
131
+
132
+ const data = await apiRequest('GET', '/institutions/', null, params);
133
+ return data || [];
134
+ }
135
+
136
+ export async function getInstitution(institutionId) {
137
+ return await apiRequest('GET', `/institutions/${institutionId}/`);
138
+ }
139
+
140
+ // ============================================================
141
+ // AGREEMENTS
142
+ // ============================================================
143
+
144
+ export async function listAgreements({ limit = 100, offset = 0 } = {}) {
145
+ const data = await apiRequest('GET', '/agreements/enduser/', null, { limit, offset });
146
+ return data?.results || [];
147
+ }
148
+
149
+ export async function getAgreement(agreementId) {
150
+ return await apiRequest('GET', `/agreements/enduser/${agreementId}/`);
151
+ }
152
+
153
+ export async function createAgreement({ institutionId, maxHistoricalDays = 90, accessValidForDays = 90, accessScope = [] }) {
154
+ const body = {
155
+ institution_id: institutionId,
156
+ max_historical_days: maxHistoricalDays,
157
+ access_valid_for_days: accessValidForDays
158
+ };
159
+
160
+ if (accessScope.length > 0) {
161
+ body.access_scope = accessScope;
162
+ }
163
+
164
+ return await apiRequest('POST', '/agreements/enduser/', body);
165
+ }
166
+
167
+ export async function deleteAgreement(agreementId) {
168
+ return await apiRequest('DELETE', `/agreements/enduser/${agreementId}/`);
169
+ }
170
+
171
+ export async function acceptAgreement(agreementId, userAgent, ipAddress) {
172
+ const body = {
173
+ user_agent: userAgent,
174
+ ip_address: ipAddress
175
+ };
176
+ return await apiRequest('PUT', `/agreements/enduser/${agreementId}/accept/`, body);
177
+ }
178
+
179
+ // ============================================================
180
+ // REQUISITIONS
181
+ // ============================================================
182
+
183
+ export async function listRequisitions({ limit = 100, offset = 0 } = {}) {
184
+ const data = await apiRequest('GET', '/requisitions/', null, { limit, offset });
185
+ return data?.results || [];
186
+ }
187
+
188
+ export async function getRequisition(requisitionId) {
189
+ return await apiRequest('GET', `/requisitions/${requisitionId}/`);
190
+ }
191
+
192
+ export async function createRequisition({ redirect, institutionId, reference, agreementId, userLanguage = 'EN' }) {
193
+ const body = {
194
+ redirect,
195
+ institution_id: institutionId,
196
+ reference,
197
+ user_language: userLanguage
198
+ };
199
+
200
+ if (agreementId) {
201
+ body.agreement = agreementId;
202
+ }
203
+
204
+ return await apiRequest('POST', '/requisitions/', body);
205
+ }
206
+
207
+ export async function deleteRequisition(requisitionId) {
208
+ return await apiRequest('DELETE', `/requisitions/${requisitionId}/`);
209
+ }
210
+
211
+ // ============================================================
212
+ // ACCOUNTS
213
+ // ============================================================
214
+
215
+ export async function getAccountMetadata(accountId) {
216
+ return await apiRequest('GET', `/accounts/${accountId}/`);
217
+ }
218
+
219
+ export async function getAccountBalances(accountId) {
220
+ return await apiRequest('GET', `/accounts/${accountId}/balances/`);
221
+ }
222
+
223
+ export async function getAccountDetails(accountId) {
224
+ return await apiRequest('GET', `/accounts/${accountId}/details/`);
225
+ }
226
+
227
+ export async function getAccountTransactions(accountId) {
228
+ return await apiRequest('GET', `/accounts/${accountId}/transactions/`);
229
+ }
package/src/config.js ADDED
@@ -0,0 +1,59 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'nordigencom-cli',
5
+ schema: {
6
+ secretId: {
7
+ type: 'string',
8
+ default: ''
9
+ },
10
+ secretKey: {
11
+ type: 'string',
12
+ default: ''
13
+ },
14
+ accessToken: {
15
+ type: 'string',
16
+ default: ''
17
+ },
18
+ refreshToken: {
19
+ type: 'string',
20
+ default: ''
21
+ },
22
+ tokenExpiry: {
23
+ type: 'number',
24
+ default: 0
25
+ }
26
+ }
27
+ });
28
+
29
+ export function getConfig(key) {
30
+ return config.get(key);
31
+ }
32
+
33
+ export function setConfig(key, value) {
34
+ config.set(key, value);
35
+ }
36
+
37
+ export function getAllConfig() {
38
+ return config.store;
39
+ }
40
+
41
+ export function clearConfig() {
42
+ config.clear();
43
+ }
44
+
45
+ export function isConfigured() {
46
+ const secretId = config.get('secretId');
47
+ const secretKey = config.get('secretKey');
48
+ return !!(secretId && secretKey);
49
+ }
50
+
51
+ export function hasValidToken() {
52
+ const accessToken = config.get('accessToken');
53
+ const tokenExpiry = config.get('tokenExpiry');
54
+ if (!accessToken) return false;
55
+ // Consider token valid if it expires more than 60 seconds from now
56
+ return tokenExpiry > Date.now() + 60000;
57
+ }
58
+
59
+ export default config;