@ktmcp-cli/nordigen 1.0.0

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/lib/api.js ADDED
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Nordigen API Client
3
+ *
4
+ * @fileoverview Core API client for interacting with Nordigen endpoints
5
+ * @module lib/api
6
+ */
7
+
8
+ import fetch from 'node-fetch';
9
+ import { getConfig } from './config.js';
10
+
11
+ const BASE_URL = 'https://ob.nordigen.com';
12
+ const API_VERSION = 'v2';
13
+
14
+ /**
15
+ * API Client for Nordigen
16
+ */
17
+ export class NordigenAPI {
18
+ /**
19
+ * @param {string} [accessToken] - JWT access token
20
+ */
21
+ constructor(accessToken = null) {
22
+ this.baseURL = BASE_URL;
23
+ this.accessToken = accessToken || this.getStoredToken();
24
+ }
25
+
26
+ /**
27
+ * Get stored access token from config
28
+ * @returns {string|null}
29
+ */
30
+ getStoredToken() {
31
+ const config = getConfig();
32
+ return config.get('auth.access_token') || null;
33
+ }
34
+
35
+ /**
36
+ * Make an HTTP request to the API
37
+ *
38
+ * @param {string} method - HTTP method
39
+ * @param {string} endpoint - API endpoint (without base URL)
40
+ * @param {Object} [options={}] - Request options
41
+ * @param {Object} [options.body] - Request body
42
+ * @param {Object} [options.query] - Query parameters
43
+ * @param {boolean} [options.auth=true] - Whether to include auth header
44
+ * @returns {Promise<Object>}
45
+ * @throws {Error} API errors with status codes
46
+ */
47
+ async request(method, endpoint, options = {}) {
48
+ const { body, query, auth = true } = options;
49
+
50
+ // Build URL with query parameters
51
+ const url = new URL(`${this.baseURL}${endpoint}`);
52
+ if (query) {
53
+ Object.entries(query).forEach(([key, value]) => {
54
+ if (value !== undefined && value !== null) {
55
+ url.searchParams.append(key, value);
56
+ }
57
+ });
58
+ }
59
+
60
+ // Build headers
61
+ const headers = {
62
+ 'Content-Type': 'application/json',
63
+ 'Accept': 'application/json',
64
+ };
65
+
66
+ if (auth && this.accessToken) {
67
+ headers['Authorization'] = `Bearer ${this.accessToken}`;
68
+ }
69
+
70
+ // Build request options
71
+ const fetchOptions = {
72
+ method,
73
+ headers,
74
+ };
75
+
76
+ if (body) {
77
+ fetchOptions.body = JSON.stringify(body);
78
+ }
79
+
80
+ // Make request
81
+ const response = await fetch(url.toString(), fetchOptions);
82
+
83
+ // Handle response
84
+ const contentType = response.headers.get('content-type');
85
+ let data;
86
+
87
+ if (contentType && contentType.includes('application/json')) {
88
+ data = await response.json();
89
+ } else {
90
+ data = await response.text();
91
+ }
92
+
93
+ // Handle errors
94
+ if (!response.ok) {
95
+ const error = new Error(data.summary || data.detail || `HTTP ${response.status}: ${response.statusText}`);
96
+ error.status = response.status;
97
+ error.statusCode = data.status_code || response.status;
98
+ error.detail = data.detail;
99
+ error.summary = data.summary;
100
+ error.type = data.type;
101
+ error.response = data;
102
+ throw error;
103
+ }
104
+
105
+ return data;
106
+ }
107
+
108
+ /**
109
+ * GET request
110
+ * @param {string} endpoint - API endpoint
111
+ * @param {Object} [options] - Request options
112
+ * @returns {Promise<Object>}
113
+ */
114
+ async get(endpoint, options = {}) {
115
+ return this.request('GET', endpoint, options);
116
+ }
117
+
118
+ /**
119
+ * POST request
120
+ * @param {string} endpoint - API endpoint
121
+ * @param {Object} [body] - Request body
122
+ * @param {Object} [options] - Additional options
123
+ * @returns {Promise<Object>}
124
+ */
125
+ async post(endpoint, body = {}, options = {}) {
126
+ return this.request('POST', endpoint, { ...options, body });
127
+ }
128
+
129
+ /**
130
+ * PUT request
131
+ * @param {string} endpoint - API endpoint
132
+ * @param {Object} [body] - Request body
133
+ * @param {Object} [options] - Additional options
134
+ * @returns {Promise<Object>}
135
+ */
136
+ async put(endpoint, body = {}, options = {}) {
137
+ return this.request('PUT', endpoint, { ...options, body });
138
+ }
139
+
140
+ /**
141
+ * DELETE request
142
+ * @param {string} endpoint - API endpoint
143
+ * @param {Object} [options] - Request options
144
+ * @returns {Promise<Object>}
145
+ */
146
+ async delete(endpoint, options = {}) {
147
+ return this.request('DELETE', endpoint, options);
148
+ }
149
+
150
+ // ==================== Authentication ====================
151
+
152
+ /**
153
+ * Obtain JWT access and refresh tokens
154
+ *
155
+ * @param {string} secretId - Secret ID
156
+ * @param {string} secretKey - Secret Key
157
+ * @returns {Promise<{access: string, refresh: string, access_expires: number, refresh_expires: number}>}
158
+ */
159
+ async obtainToken(secretId, secretKey) {
160
+ return this.post('/api/v2/token/new/', {
161
+ secret_id: secretId,
162
+ secret_key: secretKey,
163
+ }, { auth: false });
164
+ }
165
+
166
+ /**
167
+ * Refresh JWT access token
168
+ *
169
+ * @param {string} refreshToken - Refresh token
170
+ * @returns {Promise<{access: string, access_expires: number}>}
171
+ */
172
+ async refreshToken(refreshToken) {
173
+ return this.post('/api/v2/token/refresh/', {
174
+ refresh: refreshToken,
175
+ }, { auth: false });
176
+ }
177
+
178
+ // ==================== Accounts ====================
179
+
180
+ /**
181
+ * Get account metadata
182
+ *
183
+ * @param {string} accountId - Account UUID
184
+ * @returns {Promise<Object>}
185
+ */
186
+ async getAccount(accountId) {
187
+ return this.get(`/api/v2/accounts/${accountId}/`);
188
+ }
189
+
190
+ /**
191
+ * Get account balances
192
+ *
193
+ * @param {string} accountId - Account UUID
194
+ * @returns {Promise<Object>}
195
+ */
196
+ async getAccountBalances(accountId) {
197
+ return this.get(`/api/v2/accounts/${accountId}/balances/`);
198
+ }
199
+
200
+ /**
201
+ * Get account details
202
+ *
203
+ * @param {string} accountId - Account UUID
204
+ * @returns {Promise<Object>}
205
+ */
206
+ async getAccountDetails(accountId) {
207
+ return this.get(`/api/v2/accounts/${accountId}/details/`);
208
+ }
209
+
210
+ /**
211
+ * Get account transactions
212
+ *
213
+ * @param {string} accountId - Account UUID
214
+ * @param {Object} [params] - Query parameters
215
+ * @param {string} [params.date_from] - Start date (YYYY-MM-DD)
216
+ * @param {string} [params.date_to] - End date (YYYY-MM-DD)
217
+ * @returns {Promise<Object>}
218
+ */
219
+ async getAccountTransactions(accountId, params = {}) {
220
+ return this.get(`/api/v2/accounts/${accountId}/transactions/`, { query: params });
221
+ }
222
+
223
+ /**
224
+ * Get premium account transactions
225
+ *
226
+ * @param {string} accountId - Account UUID
227
+ * @param {string} country - ISO 3166 country code
228
+ * @param {Object} [params] - Query parameters
229
+ * @param {string} [params.date_from] - Start date (YYYY-MM-DD)
230
+ * @param {string} [params.date_to] - End date (YYYY-MM-DD)
231
+ * @returns {Promise<Object>}
232
+ */
233
+ async getPremiumTransactions(accountId, country, params = {}) {
234
+ return this.get(`/api/v2/accounts/premium/${accountId}/transactions/`, {
235
+ query: { country, ...params }
236
+ });
237
+ }
238
+
239
+ // ==================== Institutions ====================
240
+
241
+ /**
242
+ * List all supported institutions
243
+ *
244
+ * @param {string} country - ISO 3166 country code
245
+ * @param {Object} [params] - Query parameters
246
+ * @param {boolean} [params.payments_enabled] - Filter by payment support
247
+ * @param {boolean} [params.account_selection] - Filter by account selection
248
+ * @returns {Promise<Array>}
249
+ */
250
+ async listInstitutions(country, params = {}) {
251
+ return this.get('/api/v2/institutions/', {
252
+ query: { country, ...params }
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Get institution details
258
+ *
259
+ * @param {string} institutionId - Institution ID
260
+ * @returns {Promise<Object>}
261
+ */
262
+ async getInstitution(institutionId) {
263
+ return this.get(`/api/v2/institutions/${institutionId}/`);
264
+ }
265
+
266
+ // ==================== End User Agreements ====================
267
+
268
+ /**
269
+ * List all end user agreements
270
+ *
271
+ * @param {Object} [params] - Query parameters
272
+ * @param {number} [params.limit] - Results per page
273
+ * @param {number} [params.offset] - Offset for pagination
274
+ * @returns {Promise<Object>}
275
+ */
276
+ async listAgreements(params = {}) {
277
+ return this.get('/api/v2/agreements/enduser/', { query: params });
278
+ }
279
+
280
+ /**
281
+ * Create end user agreement
282
+ *
283
+ * @param {Object} data - Agreement data
284
+ * @param {string} data.institution_id - Institution ID
285
+ * @param {number} [data.max_historical_days=90] - Max historical days
286
+ * @param {number} [data.access_valid_for_days=90] - Access validity days
287
+ * @param {Array<string>} [data.access_scope] - Access scopes
288
+ * @returns {Promise<Object>}
289
+ */
290
+ async createAgreement(data) {
291
+ return this.post('/api/v2/agreements/enduser/', data);
292
+ }
293
+
294
+ /**
295
+ * Get end user agreement by ID
296
+ *
297
+ * @param {string} agreementId - Agreement UUID
298
+ * @returns {Promise<Object>}
299
+ */
300
+ async getAgreement(agreementId) {
301
+ return this.get(`/api/v2/agreements/enduser/${agreementId}/`);
302
+ }
303
+
304
+ /**
305
+ * Delete end user agreement
306
+ *
307
+ * @param {string} agreementId - Agreement UUID
308
+ * @returns {Promise<Object>}
309
+ */
310
+ async deleteAgreement(agreementId) {
311
+ return this.delete(`/api/v2/agreements/enduser/${agreementId}/`);
312
+ }
313
+
314
+ /**
315
+ * Accept end user agreement
316
+ *
317
+ * @param {string} agreementId - Agreement UUID
318
+ * @param {Object} data - Acceptance data
319
+ * @param {string} data.user_agent - User agent string
320
+ * @param {string} data.ip_address - User IP address
321
+ * @returns {Promise<Object>}
322
+ */
323
+ async acceptAgreement(agreementId, data) {
324
+ return this.put(`/api/v2/agreements/enduser/${agreementId}/accept/`, data);
325
+ }
326
+
327
+ // ==================== Requisitions ====================
328
+
329
+ /**
330
+ * List all requisitions
331
+ *
332
+ * @param {Object} [params] - Query parameters
333
+ * @param {number} [params.limit] - Results per page
334
+ * @param {number} [params.offset] - Offset for pagination
335
+ * @returns {Promise<Object>}
336
+ */
337
+ async listRequisitions(params = {}) {
338
+ return this.get('/api/v2/requisitions/', { query: params });
339
+ }
340
+
341
+ /**
342
+ * Create requisition
343
+ *
344
+ * @param {Object} data - Requisition data
345
+ * @param {string} data.redirect - Redirect URL after auth
346
+ * @param {string} data.institution_id - Institution ID
347
+ * @param {string} [data.reference] - Custom reference
348
+ * @param {string} [data.agreement] - Agreement UUID
349
+ * @param {string} [data.user_language] - User language code
350
+ * @param {boolean} [data.account_selection] - Enable account selection
351
+ * @param {boolean} [data.redirect_immediate] - Redirect immediately
352
+ * @returns {Promise<Object>}
353
+ */
354
+ async createRequisition(data) {
355
+ return this.post('/api/v2/requisitions/', data);
356
+ }
357
+
358
+ /**
359
+ * Get requisition by ID
360
+ *
361
+ * @param {string} requisitionId - Requisition UUID
362
+ * @returns {Promise<Object>}
363
+ */
364
+ async getRequisition(requisitionId) {
365
+ return this.get(`/api/v2/requisitions/${requisitionId}/`);
366
+ }
367
+
368
+ /**
369
+ * Delete requisition
370
+ *
371
+ * @param {string} requisitionId - Requisition UUID
372
+ * @returns {Promise<Object>}
373
+ */
374
+ async deleteRequisition(requisitionId) {
375
+ return this.delete(`/api/v2/requisitions/${requisitionId}/`);
376
+ }
377
+
378
+ // ==================== Payments ====================
379
+
380
+ /**
381
+ * List payments
382
+ *
383
+ * @param {Object} [params] - Query parameters
384
+ * @param {number} [params.limit] - Results per page
385
+ * @param {number} [params.offset] - Offset for pagination
386
+ * @returns {Promise<Object>}
387
+ */
388
+ async listPayments(params = {}) {
389
+ return this.get('/api/v2/payments/', { query: params });
390
+ }
391
+
392
+ /**
393
+ * Create payment
394
+ *
395
+ * @param {Object} data - Payment data
396
+ * @returns {Promise<Object>}
397
+ */
398
+ async createPayment(data) {
399
+ return this.post('/api/v2/payments/', data);
400
+ }
401
+
402
+ /**
403
+ * Get payment by ID
404
+ *
405
+ * @param {string} paymentId - Payment UUID
406
+ * @returns {Promise<Object>}
407
+ */
408
+ async getPayment(paymentId) {
409
+ return this.get(`/api/v2/payments/${paymentId}/`);
410
+ }
411
+
412
+ /**
413
+ * Delete periodic payment
414
+ *
415
+ * @param {string} paymentId - Payment UUID
416
+ * @returns {Promise<Object>}
417
+ */
418
+ async deletePayment(paymentId) {
419
+ return this.delete(`/api/v2/payments/${paymentId}/`);
420
+ }
421
+
422
+ /**
423
+ * List payment creditor accounts
424
+ *
425
+ * @param {Object} [params] - Query parameters
426
+ * @returns {Promise<Object>}
427
+ */
428
+ async listCreditorAccounts(params = {}) {
429
+ return this.get('/api/v2/payments/account/', { query: params });
430
+ }
431
+
432
+ /**
433
+ * List payment creditors
434
+ *
435
+ * @param {Object} [params] - Query parameters
436
+ * @returns {Promise<Object>}
437
+ */
438
+ async listCreditors(params = {}) {
439
+ return this.get('/api/v2/payments/creditors/', { query: params });
440
+ }
441
+
442
+ /**
443
+ * Create payment creditor
444
+ *
445
+ * @param {Object} data - Creditor data
446
+ * @returns {Promise<Object>}
447
+ */
448
+ async createCreditor(data) {
449
+ return this.post('/api/v2/payments/creditors/', data);
450
+ }
451
+
452
+ /**
453
+ * Get payment creditor
454
+ *
455
+ * @param {string} creditorId - Creditor UUID
456
+ * @returns {Promise<Object>}
457
+ */
458
+ async getCreditor(creditorId) {
459
+ return this.get(`/api/v2/payments/creditors/${creditorId}/`);
460
+ }
461
+
462
+ /**
463
+ * Delete payment creditor
464
+ *
465
+ * @param {string} creditorId - Creditor UUID
466
+ * @returns {Promise<Object>}
467
+ */
468
+ async deleteCreditor(creditorId) {
469
+ return this.delete(`/api/v2/payments/creditors/${creditorId}/`);
470
+ }
471
+
472
+ /**
473
+ * Get minimum required fields for institution payments
474
+ *
475
+ * @param {string} institutionId - Institution ID
476
+ * @returns {Promise<Object>}
477
+ */
478
+ async getPaymentFields(institutionId) {
479
+ return this.get(`/api/v2/payments/fields/${institutionId}/`);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Create API client instance
485
+ *
486
+ * @param {string} [accessToken] - JWT access token
487
+ * @returns {NordigenAPI}
488
+ */
489
+ export function createClient(accessToken = null) {
490
+ return new NordigenAPI(accessToken);
491
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Authentication Management
3
+ *
4
+ * @fileoverview Handles JWT token authentication and refresh
5
+ * @module lib/auth
6
+ */
7
+
8
+ import { getConfig, isAccessTokenValid, isRefreshTokenValid } from './config.js';
9
+ import { createClient } from './api.js';
10
+
11
+ /**
12
+ * Ensure valid access token, refreshing if necessary
13
+ *
14
+ * @returns {Promise<string>} Valid access token
15
+ * @throws {Error} If authentication fails
16
+ */
17
+ export async function ensureAuth() {
18
+ const config = getConfig();
19
+
20
+ // Check if access token is valid
21
+ if (isAccessTokenValid()) {
22
+ return config.get('auth.access_token');
23
+ }
24
+
25
+ // Try to refresh token
26
+ if (isRefreshTokenValid()) {
27
+ const refreshToken = config.get('auth.refresh_token');
28
+ const api = createClient();
29
+
30
+ try {
31
+ const result = await api.refreshToken(refreshToken);
32
+
33
+ // Save new access token
34
+ const now = Math.floor(Date.now() / 1000);
35
+ config.set('auth.access_token', result.access);
36
+ config.set('auth.access_expires', now + result.access_expires);
37
+
38
+ return result.access;
39
+ } catch (error) {
40
+ throw new Error('Failed to refresh token. Please login again using: nordigen auth login');
41
+ }
42
+ }
43
+
44
+ // No valid tokens
45
+ throw new Error('Not authenticated. Please login using: nordigen auth login --secret-id <id> --secret-key <key>');
46
+ }
47
+
48
+ /**
49
+ * Login with secret credentials
50
+ *
51
+ * @param {string} secretId - Secret ID
52
+ * @param {string} secretKey - Secret Key
53
+ * @returns {Promise<Object>} Token response
54
+ */
55
+ export async function login(secretId, secretKey) {
56
+ const api = createClient();
57
+ const result = await api.obtainToken(secretId, secretKey);
58
+
59
+ // Save credentials and tokens
60
+ const config = getConfig();
61
+ const now = Math.floor(Date.now() / 1000);
62
+
63
+ config.set('auth.secret_id', secretId);
64
+ config.set('auth.secret_key', secretKey);
65
+ config.set('auth.access_token', result.access);
66
+ config.set('auth.refresh_token', result.refresh);
67
+ config.set('auth.access_expires', now + result.access_expires);
68
+ config.set('auth.refresh_expires', now + result.refresh_expires);
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Logout (clear credentials)
75
+ */
76
+ export function logout() {
77
+ const config = getConfig();
78
+ config.delete('auth');
79
+ }
80
+
81
+ /**
82
+ * Check if authenticated
83
+ *
84
+ * @returns {boolean}
85
+ */
86
+ export function isAuthenticated() {
87
+ return isAccessTokenValid() || isRefreshTokenValid();
88
+ }
89
+
90
+ /**
91
+ * Get authentication status
92
+ *
93
+ * @returns {Object} Authentication status details
94
+ */
95
+ export function getAuthStatus() {
96
+ const config = getConfig();
97
+ const accessToken = config.get('auth.access_token');
98
+ const refreshToken = config.get('auth.refresh_token');
99
+ const accessExpires = config.get('auth.access_expires');
100
+ const refreshExpires = config.get('auth.refresh_expires');
101
+
102
+ const now = Math.floor(Date.now() / 1000);
103
+
104
+ return {
105
+ authenticated: isAuthenticated(),
106
+ hasAccessToken: !!accessToken,
107
+ hasRefreshToken: !!refreshToken,
108
+ accessTokenValid: isAccessTokenValid(),
109
+ refreshTokenValid: isRefreshTokenValid(),
110
+ accessExpiresIn: accessExpires ? Math.max(0, accessExpires - now) : 0,
111
+ refreshExpiresIn: refreshExpires ? Math.max(0, refreshExpires - now) : 0,
112
+ };
113
+ }