@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/AGENT.md +153 -434
- package/LICENSE +1 -1
- package/README.md +141 -171
- package/bin/nordigen.js +2 -79
- package/package.json +21 -21
- package/src/api.js +229 -0
- package/src/config.js +59 -0
- package/src/index.js +633 -0
- package/.env.example +0 -11
- package/.eslintrc.json +0 -17
- package/CHANGELOG.md +0 -69
- package/CONTRIBUTING.md +0 -198
- package/EXAMPLES.md +0 -561
- package/INDEX.md +0 -193
- package/OPENCLAW.md +0 -468
- package/PROJECT.md +0 -366
- package/QUICKREF.md +0 -231
- package/SETUP.md +0 -259
- package/SUMMARY.md +0 -419
- package/banner.png +0 -0
- package/banner.svg +0 -25
- package/logo.png +0 -0
- package/scripts/quickstart.sh +0 -110
- package/src/commands/accounts.js +0 -205
- package/src/commands/agreements.js +0 -241
- package/src/commands/auth.js +0 -86
- package/src/commands/config.js +0 -173
- package/src/commands/institutions.js +0 -181
- package/src/commands/payments.js +0 -228
- package/src/commands/requisitions.js +0 -239
- package/src/lib/api.js +0 -491
- package/src/lib/auth.js +0 -113
- package/src/lib/config.js +0 -145
- package/src/lib/output.js +0 -255
- package/src/lib/welcome.js +0 -47
- package/test/api.test.js +0 -88
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;
|