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.
@@ -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;
@@ -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;