maib-mia-sdk 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/LICENSE +674 -0
- package/README.md +146 -0
- package/package.json +56 -0
- package/src/MaibMiaApiRequest.js +339 -0
- package/src/MaibMiaAuthRequest.js +49 -0
- package/src/MaibMiaSdk.js +219 -0
- package/src/constants.js +61 -0
- package/src/errors.js +32 -0
- package/src/index.js +21 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js SDK for maib MIA API
|
|
3
|
+
* Main Class
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { name: packageName, version: packageVersion } = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const axios = require('axios');
|
|
10
|
+
|
|
11
|
+
const { SANDBOX_BASE_URL, DEFAULT_BASE_URL, DEFAULT_TIMEOUT } = require('./constants');
|
|
12
|
+
const { MaibMiaApiError, MaibMiaValidationError } = require('./errors');
|
|
13
|
+
|
|
14
|
+
class MaibMiaSdk {
|
|
15
|
+
/**
|
|
16
|
+
* Create a new MaibMiaSdk instance
|
|
17
|
+
* @param {string} baseUrl - maib MIA API base url
|
|
18
|
+
* @param {number} timeout - API request timeout in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
constructor(baseUrl = DEFAULT_BASE_URL, timeout = DEFAULT_TIMEOUT) {
|
|
21
|
+
this.baseUrl = baseUrl;
|
|
22
|
+
this.timeout = timeout;
|
|
23
|
+
|
|
24
|
+
this.client = axios.create({
|
|
25
|
+
baseURL: baseUrl,
|
|
26
|
+
timeout: timeout,
|
|
27
|
+
headers: {
|
|
28
|
+
'User-Agent': `${packageName}-node/${packageVersion}`
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setupLogging() {
|
|
34
|
+
this.client.interceptors.request.use(
|
|
35
|
+
(config) => {
|
|
36
|
+
const logData = MaibMiaSdk._getLogData(config, config);
|
|
37
|
+
console.debug(`${packageName} Request: ${logData.method} ${logData.url}`, logData);
|
|
38
|
+
return config;
|
|
39
|
+
},
|
|
40
|
+
(error) => {
|
|
41
|
+
console.error(`${packageName} Request: ${error.message}`, error);
|
|
42
|
+
return Promise.reject(error);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
this.client.interceptors.response.use(
|
|
47
|
+
(response) => {
|
|
48
|
+
const logData = MaibMiaSdk._getLogData(response, response?.config);
|
|
49
|
+
console.debug(`${packageName} Response: ${logData.status} ${logData.method} ${logData.url}`, logData);
|
|
50
|
+
return response;
|
|
51
|
+
},
|
|
52
|
+
(error) => {
|
|
53
|
+
const config = error.response?.config || error.config;
|
|
54
|
+
const logData = MaibMiaSdk._getLogData(error.response, config);
|
|
55
|
+
console.error(`${packageName} Error: ${logData.status ?? ''} ${logData.data ?? ''}`, logData, error);
|
|
56
|
+
return Promise.reject(error);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static _getLogData(object, config) {
|
|
62
|
+
const logData = {
|
|
63
|
+
'method': config?.method?.toUpperCase(),
|
|
64
|
+
'url': config ? axios.getUri(config) : undefined,
|
|
65
|
+
'data': object?.data,
|
|
66
|
+
'params': config?.params,
|
|
67
|
+
// 'headers': object?.headers?.toJSON?.() || config?.headers?.toJSON?.(),
|
|
68
|
+
'status': object?.status
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return logData;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sandbox base URL
|
|
76
|
+
*/
|
|
77
|
+
static get SANDBOX_BASE_URL() {
|
|
78
|
+
return SANDBOX_BASE_URL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Production base URL
|
|
83
|
+
*/
|
|
84
|
+
static get DEFAULT_BASE_URL() {
|
|
85
|
+
return DEFAULT_BASE_URL;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Default API request timeout in milliseconds
|
|
90
|
+
*/
|
|
91
|
+
static get DEFAULT_TIMEOUT() {
|
|
92
|
+
return DEFAULT_TIMEOUT;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Perform API request
|
|
97
|
+
* @param {string} method - Request HTTP method
|
|
98
|
+
* @param {string} url - Request URL
|
|
99
|
+
* @param {Object} data - Request data
|
|
100
|
+
* @param {Object} params - Request params
|
|
101
|
+
* @param {string} token - Access token
|
|
102
|
+
* @returns {Promise<Object>} API request response
|
|
103
|
+
*/
|
|
104
|
+
async _sendRequest(method, url, data = null, params = null, token = null) {
|
|
105
|
+
const requestConfig = {
|
|
106
|
+
url: url,
|
|
107
|
+
method: method,
|
|
108
|
+
data: data,
|
|
109
|
+
headers: {
|
|
110
|
+
...this.client.defaults.headers.common,
|
|
111
|
+
...MaibMiaSdk._getAuthHeaders(token)
|
|
112
|
+
},
|
|
113
|
+
params: params,
|
|
114
|
+
// https://github.com/axios/axios/issues/41
|
|
115
|
+
validateStatus: () => true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await this.client.request(requestConfig);
|
|
119
|
+
return MaibMiaSdk._handleResponse(response, url);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set authorization header
|
|
124
|
+
* @param {string} token - Access token
|
|
125
|
+
* @returns {Object} - Headers object
|
|
126
|
+
*/
|
|
127
|
+
static _getAuthHeaders(token) {
|
|
128
|
+
if (!token) return null;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
'Authorization': `Bearer ${token}`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handles errors returned by the API
|
|
137
|
+
* @param {axios.AxiosResponse} response - Response object
|
|
138
|
+
* @param {string} endpoint - API endpoint
|
|
139
|
+
* @throws {MaibMiaApiError} - When received a server error from the API
|
|
140
|
+
*/
|
|
141
|
+
static _handleResponse(response, endpoint) {
|
|
142
|
+
if (!response.data)
|
|
143
|
+
throw new MaibMiaApiError(`Invalid response received from server for endpoint ${endpoint}`, response);
|
|
144
|
+
|
|
145
|
+
if (response.data.ok) {
|
|
146
|
+
if (response.data.result)
|
|
147
|
+
return response.data.result;
|
|
148
|
+
|
|
149
|
+
throw new MaibMiaApiError(`Invalid response received from server for endpoint ${endpoint}: missing 'result' field`, response);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (response.data.errors && response.data.errors.length > 0) {
|
|
153
|
+
const errorMessages = response.data.errors.map(error => `${error.errorMessage} (${error.errorCode})`).join('; ');
|
|
154
|
+
throw new MaibMiaApiError(`Error sending request to endpoint ${endpoint}: ${errorMessages}`, response);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new MaibMiaApiError(`Invalid response received from server for endpoint ${endpoint}: missing 'ok' and 'errors' fields`, response);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate callback data signature
|
|
162
|
+
* @param {Object} callbackData - The callback data received from the payment gateway
|
|
163
|
+
* @param {string} signatureKey - The signature key for validation
|
|
164
|
+
* @returns {boolean} - True if signature is valid, false otherwise
|
|
165
|
+
* @link https://docs.maibmerchants.md/mia-qr-api/en/notifications-on-callback-url
|
|
166
|
+
* @link https://docs.maibmerchants.md/mia-qr-api/en/examples/signature-key-verification
|
|
167
|
+
* @link https://docs.maibmerchants.md/request-to-pay/api-reference/callback-notifications#signature-validation
|
|
168
|
+
* @link https://docs.maibmerchants.md/request-to-pay/api-reference/examples/signature-key-verification
|
|
169
|
+
* @throws {MaibMiaValidationError} - If Callback data or Signature Key are invalid
|
|
170
|
+
*/
|
|
171
|
+
static validateCallbackSignature(callbackData, signatureKey) {
|
|
172
|
+
if (!signatureKey) {
|
|
173
|
+
throw new MaibMiaValidationError('Invalid signature key');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!callbackData?.signature || !callbackData?.result) {
|
|
177
|
+
throw new MaibMiaValidationError('Missing result or signature in callback data');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const computedResultSignature = MaibMiaSdk.computeDataSignature(callbackData.result, signatureKey);
|
|
181
|
+
return crypto.timingSafeEqual(Buffer.from(computedResultSignature), Buffer.from(callbackData.signature));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static computeDataSignature(resultData, signatureKey) {
|
|
185
|
+
const keys = {};
|
|
186
|
+
|
|
187
|
+
// Collect and format values
|
|
188
|
+
for (const [key, value] of Object.entries(resultData)) {
|
|
189
|
+
if (value === null || value === undefined) continue;
|
|
190
|
+
|
|
191
|
+
let valueStr;
|
|
192
|
+
if (key === 'amount' || key === 'commission') {
|
|
193
|
+
valueStr = parseFloat(value).toFixed(2); // Always two decimals
|
|
194
|
+
} else {
|
|
195
|
+
valueStr = String(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (valueStr.trim() !== '') {
|
|
199
|
+
keys[key] = valueStr;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Sort keys case-insensitively
|
|
204
|
+
const orderedKeys = Object.keys(keys).sort((a, b) =>
|
|
205
|
+
a.toLowerCase().localeCompare(b.toLowerCase())
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Join values with colon
|
|
209
|
+
const additionalString = orderedKeys.map(k => keys[k]).join(':');
|
|
210
|
+
const hashInput = `${additionalString}:${signatureKey}`;
|
|
211
|
+
|
|
212
|
+
// Hash and base64 encode
|
|
213
|
+
const hash = crypto.createHash('sha256').update(hashInput, 'utf8').digest('base64');
|
|
214
|
+
|
|
215
|
+
return hash;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = MaibMiaSdk;
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js SDK for maib MIA API
|
|
3
|
+
* Constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// maib MIA QR API base urls
|
|
7
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/overview/general-technical-specifications#available-base-urls
|
|
8
|
+
// https://docs.maibmerchants.md/request-to-pay/getting-started/api-fundamentals#available-environments
|
|
9
|
+
const DEFAULT_BASE_URL = 'https://api.maibmerchants.md/v2/';
|
|
10
|
+
const SANDBOX_BASE_URL = 'https://sandbox.maibmerchants.md/v2/';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT = 30000; // milliseconds
|
|
13
|
+
|
|
14
|
+
const API_ENDPOINTS = {
|
|
15
|
+
AUTH_TOKEN: 'auth/token',
|
|
16
|
+
|
|
17
|
+
// maib MIA QR API endpoints
|
|
18
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/endpoints
|
|
19
|
+
MIA_QR: 'mia/qr',
|
|
20
|
+
MIA_QR_HYBRID: 'mia/qr/hybrid',
|
|
21
|
+
MIA_QR_ID: 'mia/qr/:qrId',
|
|
22
|
+
MIA_QR_EXTENSION: 'mia/qr/:qrId/extension',
|
|
23
|
+
MIA_QR_CANCEL: 'mia/qr/:qrId/cancel',
|
|
24
|
+
MIA_QR_EXTENSION_CANCEL: 'mia/qr/:qrId/extension/cancel',
|
|
25
|
+
MIA_PAYMENTS: 'mia/payments',
|
|
26
|
+
MIA_PAYMENTS_ID: 'mia/payments/:payId',
|
|
27
|
+
MIA_PAYMENTS_REFUND: 'mia/payments/:payId/refund',
|
|
28
|
+
MIA_TEST_PAY: 'mia/test-pay',
|
|
29
|
+
|
|
30
|
+
// maib RTP API endpoints
|
|
31
|
+
// https://docs.maibmerchants.md/request-to-pay/api-reference/endpoints
|
|
32
|
+
MIA_RTP: 'rtp',
|
|
33
|
+
MIA_RTP_ID: 'rtp/:rtpId',
|
|
34
|
+
MIA_RTP_CANCEL: 'rtp/:rtpId/cancel',
|
|
35
|
+
MIA_RTP_REFUND: 'rtp/:payId/refund',
|
|
36
|
+
MIA_RTP_TEST_ACCEPT: 'rtp/:rtpId/test-accept',
|
|
37
|
+
MIA_RTP_TEST_REJECT: 'rtp/:rtpId/test-reject'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const REQUIRED_PARAMS = {
|
|
41
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/endpoints/payment-initiation/create-qr-code-static-dynamic#request-parameters-body
|
|
42
|
+
QR_PARAMS: ['type', 'amountType', 'currency', 'description'],
|
|
43
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/endpoints/payment-initiation/create-hybrid-qr-code#request-body-parameters
|
|
44
|
+
QR_HYBRID_PARAMS: ['amountType', 'currency'],
|
|
45
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/endpoints/payment-initiation/create-hybrid-qr-code/create-extension-for-qr-code-by-id#request-parameters-body
|
|
46
|
+
QR_EXTENSION_PARAMS: ['expiresAt', 'description'],
|
|
47
|
+
// https://docs.maibmerchants.md/mia-qr-api/en/payment-simulation-sandbox#request-parameters-body-json
|
|
48
|
+
TEST_PAY_PARAMS: ['qrId', 'amount', 'iban', 'currency', 'payerName'],
|
|
49
|
+
// https://docs.maibmerchants.md/request-to-pay/api-reference/endpoints/create-a-new-payment-request-rtp#request-body-parameters
|
|
50
|
+
RTP_PARAMS: ['alias', 'amount', 'currency', 'expiresAt', 'description'],
|
|
51
|
+
// https://docs.maibmerchants.md/request-to-pay/api-reference/sandbox-simulation-environment/simulate-acceptance-of-a-payment-request#request-body-parameters
|
|
52
|
+
TEST_ACCEPT_PARAMS: ['amount', 'currency']
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
DEFAULT_BASE_URL,
|
|
57
|
+
SANDBOX_BASE_URL,
|
|
58
|
+
DEFAULT_TIMEOUT,
|
|
59
|
+
API_ENDPOINTS,
|
|
60
|
+
REQUIRED_PARAMS
|
|
61
|
+
};
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js SDK for maib MIA API
|
|
3
|
+
* Custom Error Classes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class MaibMiaError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class MaibMiaApiError extends MaibMiaError {
|
|
15
|
+
constructor(message, response = null, error = null) {
|
|
16
|
+
super(message, { cause: error });
|
|
17
|
+
|
|
18
|
+
this.response = response;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class MaibMiaValidationError extends MaibMiaError {
|
|
23
|
+
constructor(message, error = null) {
|
|
24
|
+
super(message, { cause: error });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
MaibMiaError,
|
|
30
|
+
MaibMiaApiError,
|
|
31
|
+
MaibMiaValidationError,
|
|
32
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js SDK for maib MIA API
|
|
3
|
+
* Main Entry Point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MaibMiaSdk = require('./MaibMiaSdk');
|
|
7
|
+
const MaibMiaAuthRequest = require('./MaibMiaAuthRequest');
|
|
8
|
+
const MaibMiaApiRequest = require('./MaibMiaApiRequest');
|
|
9
|
+
const { MaibMiaError, MaibMiaApiError, MaibMiaValidationError } = require('./errors');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
MaibMiaSdk,
|
|
13
|
+
MaibMiaAuthRequest,
|
|
14
|
+
MaibMiaApiRequest,
|
|
15
|
+
MaibMiaError,
|
|
16
|
+
MaibMiaApiError,
|
|
17
|
+
MaibMiaValidationError,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Default export for ES6 imports
|
|
21
|
+
module.exports.default = MaibMiaSdk;
|