@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/.env.example +11 -0
- package/.eslintrc.json +17 -0
- package/AGENT.md +480 -0
- package/CHANGELOG.md +69 -0
- package/CONTRIBUTING.md +198 -0
- package/EXAMPLES.md +561 -0
- package/INDEX.md +193 -0
- package/LICENSE +21 -0
- package/OPENCLAW.md +468 -0
- package/PROJECT.md +366 -0
- package/QUICKREF.md +231 -0
- package/README.md +424 -0
- package/SETUP.md +259 -0
- package/SUMMARY.md +419 -0
- package/banner.png +0 -0
- package/bin/nordigen.js +84 -0
- package/logo.png +0 -0
- package/package.json +40 -0
- package/scripts/quickstart.sh +110 -0
- package/src/commands/accounts.js +205 -0
- package/src/commands/agreements.js +241 -0
- package/src/commands/auth.js +86 -0
- package/src/commands/config.js +173 -0
- package/src/commands/institutions.js +181 -0
- package/src/commands/payments.js +228 -0
- package/src/commands/requisitions.js +239 -0
- package/src/lib/api.js +491 -0
- package/src/lib/auth.js +113 -0
- package/src/lib/config.js +145 -0
- package/src/lib/output.js +255 -0
- package/test/api.test.js +88 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Management
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Manages CLI configuration and credentials storage
|
|
5
|
+
* @module lib/config
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Conf from 'conf';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration schema
|
|
14
|
+
*/
|
|
15
|
+
const schema = {
|
|
16
|
+
auth: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
secret_id: { type: 'string' },
|
|
20
|
+
secret_key: { type: 'string' },
|
|
21
|
+
access_token: { type: 'string' },
|
|
22
|
+
refresh_token: { type: 'string' },
|
|
23
|
+
access_expires: { type: 'number' },
|
|
24
|
+
refresh_expires: { type: 'number' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaults: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
country: { type: 'string' },
|
|
31
|
+
institution_id: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let configInstance = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get or create config instance
|
|
40
|
+
*
|
|
41
|
+
* @returns {Conf}
|
|
42
|
+
*/
|
|
43
|
+
export function getConfig() {
|
|
44
|
+
if (!configInstance) {
|
|
45
|
+
configInstance = new Conf({
|
|
46
|
+
projectName: 'nordigen-cli',
|
|
47
|
+
schema,
|
|
48
|
+
configFileMode: 0o600, // Read/write for owner only
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return configInstance;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get configuration value
|
|
56
|
+
*
|
|
57
|
+
* @param {string} key - Configuration key (dot notation supported)
|
|
58
|
+
* @param {*} [defaultValue] - Default value if key doesn't exist
|
|
59
|
+
* @returns {*}
|
|
60
|
+
*/
|
|
61
|
+
export function get(key, defaultValue) {
|
|
62
|
+
return getConfig().get(key, defaultValue);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set configuration value
|
|
67
|
+
*
|
|
68
|
+
* @param {string} key - Configuration key (dot notation supported)
|
|
69
|
+
* @param {*} value - Value to set
|
|
70
|
+
*/
|
|
71
|
+
export function set(key, value) {
|
|
72
|
+
getConfig().set(key, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete configuration value
|
|
77
|
+
*
|
|
78
|
+
* @param {string} key - Configuration key
|
|
79
|
+
*/
|
|
80
|
+
export function del(key) {
|
|
81
|
+
getConfig().delete(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clear all configuration
|
|
86
|
+
*/
|
|
87
|
+
export function clear() {
|
|
88
|
+
getConfig().clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all configuration
|
|
93
|
+
*
|
|
94
|
+
* @returns {Object}
|
|
95
|
+
*/
|
|
96
|
+
export function getAll() {
|
|
97
|
+
return getConfig().store;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if access token is valid (not expired)
|
|
102
|
+
*
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
export function isAccessTokenValid() {
|
|
106
|
+
const config = getConfig();
|
|
107
|
+
const token = config.get('auth.access_token');
|
|
108
|
+
const expires = config.get('auth.access_expires');
|
|
109
|
+
|
|
110
|
+
if (!token || !expires) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if token expires in more than 60 seconds
|
|
115
|
+
const now = Math.floor(Date.now() / 1000);
|
|
116
|
+
return expires > (now + 60);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if refresh token is valid (not expired)
|
|
121
|
+
*
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
export function isRefreshTokenValid() {
|
|
125
|
+
const config = getConfig();
|
|
126
|
+
const token = config.get('auth.refresh_token');
|
|
127
|
+
const expires = config.get('auth.refresh_expires');
|
|
128
|
+
|
|
129
|
+
if (!token || !expires) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if token expires in more than 60 seconds
|
|
134
|
+
const now = Math.floor(Date.now() / 1000);
|
|
135
|
+
return expires > (now + 60);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get config file path
|
|
140
|
+
*
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
export function getConfigPath() {
|
|
144
|
+
return getConfig().path;
|
|
145
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Formatting
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Utilities for formatting and displaying CLI output
|
|
5
|
+
* @module lib/output
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { format as formatDate } from 'date-fns';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Output formats
|
|
13
|
+
*/
|
|
14
|
+
export const OutputFormat = {
|
|
15
|
+
JSON: 'json',
|
|
16
|
+
TABLE: 'table',
|
|
17
|
+
LIST: 'list',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Print JSON output
|
|
22
|
+
*
|
|
23
|
+
* @param {*} data - Data to output
|
|
24
|
+
* @param {boolean} [pretty=true] - Pretty print
|
|
25
|
+
*/
|
|
26
|
+
export function printJSON(data, pretty = true) {
|
|
27
|
+
if (pretty) {
|
|
28
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(JSON.stringify(data));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print success message
|
|
36
|
+
*
|
|
37
|
+
* @param {string} message - Success message
|
|
38
|
+
*/
|
|
39
|
+
export function printSuccess(message) {
|
|
40
|
+
console.log(chalk.green('✓'), message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Print error message
|
|
45
|
+
*
|
|
46
|
+
* @param {string} message - Error message
|
|
47
|
+
* @param {Error} [error] - Error object
|
|
48
|
+
*/
|
|
49
|
+
export function printError(message, error = null) {
|
|
50
|
+
console.error(chalk.red('✗'), message);
|
|
51
|
+
if (error && process.env.DEBUG) {
|
|
52
|
+
console.error(chalk.gray(error.stack));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Print warning message
|
|
58
|
+
*
|
|
59
|
+
* @param {string} message - Warning message
|
|
60
|
+
*/
|
|
61
|
+
export function printWarning(message) {
|
|
62
|
+
console.log(chalk.yellow('⚠'), message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Print info message
|
|
67
|
+
*
|
|
68
|
+
* @param {string} message - Info message
|
|
69
|
+
*/
|
|
70
|
+
export function printInfo(message) {
|
|
71
|
+
console.log(chalk.blue('ℹ'), message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Print table row
|
|
76
|
+
*
|
|
77
|
+
* @param {string} label - Row label
|
|
78
|
+
* @param {string} value - Row value
|
|
79
|
+
* @param {number} [labelWidth=20] - Label column width
|
|
80
|
+
*/
|
|
81
|
+
export function printRow(label, value, labelWidth = 20) {
|
|
82
|
+
const paddedLabel = label.padEnd(labelWidth);
|
|
83
|
+
console.log(`${chalk.cyan(paddedLabel)} ${value}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Print section header
|
|
88
|
+
*
|
|
89
|
+
* @param {string} title - Section title
|
|
90
|
+
*/
|
|
91
|
+
export function printSection(title) {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(chalk.bold.underline(title));
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format currency amount
|
|
99
|
+
*
|
|
100
|
+
* @param {number|string} amount - Amount
|
|
101
|
+
* @param {string} [currency='EUR'] - Currency code
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function formatCurrency(amount, currency = 'EUR') {
|
|
105
|
+
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
106
|
+
return new Intl.NumberFormat('en-US', {
|
|
107
|
+
style: 'currency',
|
|
108
|
+
currency: currency,
|
|
109
|
+
}).format(num);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format date
|
|
114
|
+
*
|
|
115
|
+
* @param {string|Date} date - Date to format
|
|
116
|
+
* @param {string} [format='yyyy-MM-dd'] - Date format
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function formatDateString(date, format = 'yyyy-MM-dd') {
|
|
120
|
+
if (!date) return 'N/A';
|
|
121
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
122
|
+
return formatDate(d, format);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format relative time
|
|
127
|
+
*
|
|
128
|
+
* @param {string|Date} date - Date to format
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function formatRelativeTime(date) {
|
|
132
|
+
if (!date) return 'N/A';
|
|
133
|
+
|
|
134
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const diffMs = now - d;
|
|
137
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
138
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
139
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
140
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
141
|
+
|
|
142
|
+
if (diffSec < 60) return 'just now';
|
|
143
|
+
if (diffMin < 60) return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`;
|
|
144
|
+
if (diffHour < 24) return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`;
|
|
145
|
+
if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`;
|
|
146
|
+
|
|
147
|
+
return formatDate(d, 'yyyy-MM-dd');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format seconds to human-readable duration
|
|
152
|
+
*
|
|
153
|
+
* @param {number} seconds - Duration in seconds
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
export function formatDuration(seconds) {
|
|
157
|
+
if (seconds < 60) return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
158
|
+
if (seconds < 3600) {
|
|
159
|
+
const mins = Math.floor(seconds / 60);
|
|
160
|
+
return `${mins} minute${mins !== 1 ? 's' : ''}`;
|
|
161
|
+
}
|
|
162
|
+
if (seconds < 86400) {
|
|
163
|
+
const hours = Math.floor(seconds / 3600);
|
|
164
|
+
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
165
|
+
}
|
|
166
|
+
const days = Math.floor(seconds / 86400);
|
|
167
|
+
return `${days} day${days !== 1 ? 's' : ''}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Print account summary
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} account - Account data
|
|
174
|
+
*/
|
|
175
|
+
export function printAccountSummary(account) {
|
|
176
|
+
printSection('Account Details');
|
|
177
|
+
printRow('Account ID', account.id);
|
|
178
|
+
printRow('IBAN', account.iban || 'N/A');
|
|
179
|
+
printRow('Status', formatStatus(account.status));
|
|
180
|
+
printRow('Created', formatDateString(account.created));
|
|
181
|
+
printRow('Last Accessed', formatRelativeTime(account.last_accessed));
|
|
182
|
+
if (account.institution_id) {
|
|
183
|
+
printRow('Institution ID', account.institution_id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format status with color
|
|
189
|
+
*
|
|
190
|
+
* @param {string} status - Status string
|
|
191
|
+
* @returns {string}
|
|
192
|
+
*/
|
|
193
|
+
export function formatStatus(status) {
|
|
194
|
+
switch (status?.toUpperCase()) {
|
|
195
|
+
case 'READY':
|
|
196
|
+
case 'ACTIVE':
|
|
197
|
+
case 'COMPLETED':
|
|
198
|
+
return chalk.green(status);
|
|
199
|
+
case 'PROCESSING':
|
|
200
|
+
case 'PENDING':
|
|
201
|
+
return chalk.yellow(status);
|
|
202
|
+
case 'ERROR':
|
|
203
|
+
case 'SUSPENDED':
|
|
204
|
+
case 'EXPIRED':
|
|
205
|
+
return chalk.red(status);
|
|
206
|
+
default:
|
|
207
|
+
return status || 'N/A';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Print transaction
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} transaction - Transaction data
|
|
215
|
+
*/
|
|
216
|
+
export function printTransaction(transaction) {
|
|
217
|
+
const amount = transaction.transactionAmount || {};
|
|
218
|
+
const date = transaction.bookingDate || transaction.valueDate;
|
|
219
|
+
|
|
220
|
+
console.log(
|
|
221
|
+
chalk.cyan(formatDateString(date)),
|
|
222
|
+
formatCurrency(amount.amount, amount.currency),
|
|
223
|
+
transaction.remittanceInformationUnstructured || transaction.debtorName || 'N/A'
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Print list of items
|
|
229
|
+
*
|
|
230
|
+
* @param {Array} items - Items to print
|
|
231
|
+
* @param {Function} formatter - Function to format each item
|
|
232
|
+
*/
|
|
233
|
+
export function printList(items, formatter) {
|
|
234
|
+
if (!items || items.length === 0) {
|
|
235
|
+
printWarning('No items found');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
items.forEach((item, index) => {
|
|
240
|
+
formatter(item, index);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Truncate string
|
|
246
|
+
*
|
|
247
|
+
* @param {string} str - String to truncate
|
|
248
|
+
* @param {number} [maxLength=50] - Maximum length
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function truncate(str, maxLength = 50) {
|
|
252
|
+
if (!str) return '';
|
|
253
|
+
if (str.length <= maxLength) return str;
|
|
254
|
+
return str.substring(0, maxLength - 3) + '...';
|
|
255
|
+
}
|
package/test/api.test.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { NordigenAPI } from '../src/lib/api.js';
|
|
8
|
+
|
|
9
|
+
test('NordigenAPI - initialization', () => {
|
|
10
|
+
const api = new NordigenAPI();
|
|
11
|
+
assert.ok(api);
|
|
12
|
+
assert.equal(api.baseURL, 'https://ob.nordigen.com');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('NordigenAPI - request URL building', async () => {
|
|
16
|
+
const api = new NordigenAPI('test-token');
|
|
17
|
+
|
|
18
|
+
// Mock fetch for testing
|
|
19
|
+
const originalFetch = global.fetch;
|
|
20
|
+
global.fetch = async (url) => {
|
|
21
|
+
assert.ok(url.includes('/api/v2/'));
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
25
|
+
json: async () => ({ test: 'data' })
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await api.get('/api/v2/test');
|
|
31
|
+
assert.deepEqual(result, { test: 'data' });
|
|
32
|
+
} finally {
|
|
33
|
+
global.fetch = originalFetch;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('NordigenAPI - query parameters', async () => {
|
|
38
|
+
const api = new NordigenAPI('test-token');
|
|
39
|
+
|
|
40
|
+
const originalFetch = global.fetch;
|
|
41
|
+
global.fetch = async (url) => {
|
|
42
|
+
assert.ok(url.includes('country=GB'));
|
|
43
|
+
assert.ok(url.includes('limit=10'));
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
47
|
+
json: async () => ([])
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await api.get('/api/v2/institutions/', {
|
|
53
|
+
query: { country: 'GB', limit: 10 }
|
|
54
|
+
});
|
|
55
|
+
} finally {
|
|
56
|
+
global.fetch = originalFetch;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('NordigenAPI - error handling', async () => {
|
|
61
|
+
const api = new NordigenAPI('test-token');
|
|
62
|
+
|
|
63
|
+
const originalFetch = global.fetch;
|
|
64
|
+
global.fetch = async () => ({
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 401,
|
|
67
|
+
statusText: 'Unauthorized',
|
|
68
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
69
|
+
json: async () => ({
|
|
70
|
+
status_code: 401,
|
|
71
|
+
summary: 'Invalid token',
|
|
72
|
+
detail: 'Token is invalid or expired'
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await assert.rejects(
|
|
78
|
+
async () => await api.get('/api/v2/accounts/test'),
|
|
79
|
+
(error) => {
|
|
80
|
+
assert.equal(error.status, 401);
|
|
81
|
+
assert.ok(error.message.includes('Invalid token'));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
} finally {
|
|
86
|
+
global.fetch = originalFetch;
|
|
87
|
+
}
|
|
88
|
+
});
|