redsys-easy 5.3.1 → 6.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/README.md +14 -0
- package/lib/assets/signatures.d.ts +2 -0
- package/lib/client.d.ts +3 -1
- package/lib/crypto.d.ts +22 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.esm.js +166 -29
- package/lib/index.js +168 -31
- package/lib/rest/json-serialization.d.ts +1 -1
- package/lib/rest/json-sha512-signature.d.ts +4 -0
- package/lib/rest/json.d.ts +3 -3
- package/lib/rest/requests.d.ts +3 -2
- package/lib/types/api.d.ts +27 -1
- package/lib/types/input-params.d.ts +8 -8
- package/lib/utils/signatures.d.ts +3 -0
- package/package.json +15 -42
package/README.md
CHANGED
|
@@ -132,6 +132,20 @@ Redsys API uses expects amounts denominated in the smallest unit of a currency.
|
|
|
132
132
|
|
|
133
133
|
However you are free to wrap these core functions with a custom formatter. Particularly, redsys-easy optional formatters expect the amount to be formatted as a decimal string currency unit. For euros, it would be '1.99'.
|
|
134
134
|
|
|
135
|
+
### Which signature version is used?
|
|
136
|
+
|
|
137
|
+
REST requests and redirect forms are signed with [`HMAC_SHA512_V2`](https://pagosonline.redsys.es/desarrolladores-inicio/documentacion-operativa/firmar-una-operacion/) by default. REST responses and JSON notifications allow also `HMAC_SHA512_V1` and `HMAC_SHA256_V1` signatures. To change this behavior, pass a list of allowed versions to `signatureVersions` in `createRedsysAPI` with the version to sign requests and forms as the first item. The same merchant key works for all signature versions.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
createRedsysAPI({
|
|
141
|
+
secretKey: 'sq7HjrUOBfKmC576ILgskD5srU870gJ7',
|
|
142
|
+
urls: SANDBOX_URLS,
|
|
143
|
+
signatureVersions: ['HMAC_SHA512_V2', 'HMAC_SHA512_V1', 'HMAC_SHA256_V1']
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
SOAP notifications and their responses still use `HMAC_SHA256_V1` only, and they are unaffected by the configuration above.
|
|
148
|
+
|
|
135
149
|
## Acknowledgments
|
|
136
150
|
|
|
137
151
|
Based on previous work
|
package/lib/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { fetch as Fetch } from 'undici';
|
|
2
|
-
import type { ResponseJSONSuccess, RedirectForm } from './types/api';
|
|
2
|
+
import type { ResponseJSONSuccess, RedirectForm, SignatureVersion } from './types/api';
|
|
3
3
|
import type { RestIniciaPeticionInputParams, RestTrataPeticionInputParams, RedirectInputParams } from './types/input-params';
|
|
4
4
|
import type { RestIniciaPeticionOutputParams, RestTrataPeticionOutputParams, RestNotificationOutputParams, SoapNotificationOutputParams } from './types/output-params';
|
|
5
5
|
/**
|
|
@@ -33,6 +33,8 @@ export interface RedsysConfig {
|
|
|
33
33
|
fetch?: typeof Fetch | undefined;
|
|
34
34
|
secretKey: string;
|
|
35
35
|
urls: UrlsConfig;
|
|
36
|
+
/** Allowed signature versions. The first item is used to sign requests and redirect forms. */
|
|
37
|
+
signatureVersions?: readonly SignatureVersion[] | undefined;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Sends a iniciaPeticion request using REST interface
|
package/lib/crypto.d.ts
CHANGED
|
@@ -22,3 +22,25 @@ export declare const encrypt3DES: (key: string, message: string) => Buffer;
|
|
|
22
22
|
* @params params - Payload to sign
|
|
23
23
|
*/
|
|
24
24
|
export declare const sha256Sign: (merchantKey: string, order: string, params: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Encrypt a message using AES-128-CBC
|
|
27
|
+
*
|
|
28
|
+
* @params key - Key to encrypt message, used as text
|
|
29
|
+
* @params message - Message to be encrypted
|
|
30
|
+
*/
|
|
31
|
+
export declare const encryptAES: (key: string, message: string) => Buffer;
|
|
32
|
+
/**
|
|
33
|
+
* Compute HMAC_SHA512_V2 signature
|
|
34
|
+
*
|
|
35
|
+
* @params merchantKey - Key to encrypt message
|
|
36
|
+
* @params order - Order number
|
|
37
|
+
* @params params - Payload to sign
|
|
38
|
+
*/
|
|
39
|
+
export declare const sha512Sign: (merchantKey: string, order: string, params: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Compare two signatures in constant time
|
|
42
|
+
*
|
|
43
|
+
* @params expected - Signature computed locally, base64 or base64url encoded
|
|
44
|
+
* @params received - Signature received from redsys, base64 or base64url encoded
|
|
45
|
+
*/
|
|
46
|
+
export declare const isSignatureEqual: (expected: string, received: string) => boolean;
|
package/lib/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { getResponseCodeMessage, getSISErrorCodeMessage } from './utils/codes';
|
|
|
10
10
|
export { RedsysError, ValidationError, ParseError, HTTPError, GatewayError, ResponseError } from './errors';
|
|
11
11
|
export type { RedirectInputParams, RestIniciaPeticionInputParams, RestTrataPeticionInputParams } from './types/input-params';
|
|
12
12
|
export type { RestIniciaPeticionOutputParams, RestTrataPeticionOutputParams, RestNotificationOutputParams, SoapNotificationOutputParams } from './types/output-params';
|
|
13
|
-
export type { ResponseJSONSuccess, RedirectForm, ThreeDSv2MethodNotificationBody, ThreeDSv1ChallengeNotificationBody, ThreeDSv2ChallengeNotificationBody } from './types/api';
|
|
13
|
+
export type { ResponseJSONSuccess, RedirectForm, SignatureVersion, ThreeDSv2MethodNotificationBody, ThreeDSv1ChallengeNotificationBody, ThreeDSv2ChallengeNotificationBody } from './types/api';
|
|
14
14
|
export { serializeAndSignJSONRequest, deserializeAndVerifyJSONResponse, } from './rest/json';
|
|
15
15
|
export { create3DSMethodForm, create3DSv1ChallengeForm, create3DSv2ChallengeForm, deserializeThreeDSMethodData, deserializeCres } from './rest/3ds';
|
|
16
16
|
export type { ThreeDSBrowserClientInfo, ThreeDSMethodForm, ThreeDSv1ChallengeForm, ThreeDSv2ChallengeForm, ThreeDSMethodData, ThreeDSCres } from './types/3ds-params';
|
package/lib/index.esm.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { fetch } from 'undici';
|
|
2
|
-
import { STATUS_CODES } from 'http';
|
|
3
|
-
import
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { STATUS_CODES } from 'node:http';
|
|
3
|
+
import crypto, { randomInt } from 'node:crypto';
|
|
6
4
|
import { XMLParser } from 'fast-xml-parser';
|
|
7
5
|
import Decimal from 'decimal.js';
|
|
8
6
|
import dayjs from 'dayjs';
|
|
@@ -3206,10 +3204,12 @@ const deserializeJSONMerchantParams = (strPayload) => {
|
|
|
3206
3204
|
}
|
|
3207
3205
|
return payload;
|
|
3208
3206
|
};
|
|
3209
|
-
const serializeJSONMerchantParams = (requestParams
|
|
3207
|
+
const serializeJSONMerchantParams = (requestParams,
|
|
3208
|
+
// HMAC_SHA512_V2 requires base64url without padding, redsys answers SIS0430 otherwise
|
|
3209
|
+
encoding) =>
|
|
3210
3210
|
// Official redsys docs escape "/" because they use php json_encode. I think
|
|
3211
3211
|
// this is wrong and it also works without escaping.
|
|
3212
|
-
Buffer.from(JSON.stringify(requestParams), 'utf8').toString(
|
|
3212
|
+
Buffer.from(JSON.stringify(requestParams), 'utf8').toString(encoding);
|
|
3213
3213
|
|
|
3214
3214
|
/**
|
|
3215
3215
|
* Adds padding to a buffer.
|
|
@@ -3232,6 +3232,7 @@ const zeroPad = (buf, blocksize) => {
|
|
|
3232
3232
|
const encrypt3DES = (key, message) => {
|
|
3233
3233
|
const keyBuf = Buffer.from(key, 'base64');
|
|
3234
3234
|
const iv = Buffer.alloc(8, 0);
|
|
3235
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
|
|
3235
3236
|
const messageBuf = Buffer.from(message.toString(), 'utf8');
|
|
3236
3237
|
// Align to blocksize by padding the message buffer
|
|
3237
3238
|
const paddedMessageBuf = zeroPad(messageBuf, 8);
|
|
@@ -3259,6 +3260,49 @@ const sha256Sign = (merchantKey, order, params) => {
|
|
|
3259
3260
|
.update(params)
|
|
3260
3261
|
.digest('base64');
|
|
3261
3262
|
};
|
|
3263
|
+
/**
|
|
3264
|
+
* Encrypt a message using AES-128-CBC
|
|
3265
|
+
*
|
|
3266
|
+
* @params key - Key to encrypt message, used as text
|
|
3267
|
+
* @params message - Message to be encrypted
|
|
3268
|
+
*/
|
|
3269
|
+
const encryptAES = (key, message) => {
|
|
3270
|
+
// Redsys takes the first 16 characters of the key, right padded with zeros
|
|
3271
|
+
const keyBuf = Buffer.from(key.slice(0, 16).padEnd(16, '0'), 'utf8');
|
|
3272
|
+
const iv = Buffer.alloc(16, 0);
|
|
3273
|
+
// PKCS#7 padding, the default
|
|
3274
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', keyBuf, iv);
|
|
3275
|
+
return Buffer.concat([cipher.update(message, 'utf8'), cipher.final()]);
|
|
3276
|
+
};
|
|
3277
|
+
/**
|
|
3278
|
+
* Compute HMAC_SHA512_V2 signature
|
|
3279
|
+
*
|
|
3280
|
+
* @params merchantKey - Key to encrypt message
|
|
3281
|
+
* @params order - Order number
|
|
3282
|
+
* @params params - Payload to sign
|
|
3283
|
+
*/
|
|
3284
|
+
const sha512Sign = (merchantKey, order, params) => {
|
|
3285
|
+
// The HMAC key is the encrypted order as a base64 string, not its bytes
|
|
3286
|
+
const orderKey = encryptAES(merchantKey, order).toString('base64');
|
|
3287
|
+
return crypto
|
|
3288
|
+
.createHmac('sha512', orderKey)
|
|
3289
|
+
.update(params)
|
|
3290
|
+
.digest('base64url');
|
|
3291
|
+
};
|
|
3292
|
+
/**
|
|
3293
|
+
* Compare two signatures in constant time
|
|
3294
|
+
*
|
|
3295
|
+
* @params expected - Signature computed locally, base64 or base64url encoded
|
|
3296
|
+
* @params received - Signature received from redsys, base64 or base64url encoded
|
|
3297
|
+
*/
|
|
3298
|
+
const isSignatureEqual = (expected, received) => {
|
|
3299
|
+
// Comparing different base64 encodings is messy. Make it foolproof by comparing buffers.
|
|
3300
|
+
// In node, the input encoding 'base64' accepts also base64url, with and without padding.
|
|
3301
|
+
const expectedBuf = Buffer.from(expected, 'base64');
|
|
3302
|
+
const receivedBuf = Buffer.from(received, 'base64');
|
|
3303
|
+
return (expectedBuf.length === receivedBuf.length &&
|
|
3304
|
+
crypto.timingSafeEqual(expectedBuf, receivedBuf));
|
|
3305
|
+
};
|
|
3262
3306
|
|
|
3263
3307
|
/**
|
|
3264
3308
|
* Discrete uniform distribution with domain (0 to *max*)
|
|
@@ -3276,6 +3320,7 @@ const randomTransactionId = () => {
|
|
|
3276
3320
|
const numStr = num.toString().padStart(4, '0');
|
|
3277
3321
|
// Random 8 letter alphanumeric sequence
|
|
3278
3322
|
const alphanum = new Array(8);
|
|
3323
|
+
// eslint-disable-next-line no-plusplus
|
|
3279
3324
|
for (let idx = 0; idx < 8; idx++) {
|
|
3280
3325
|
alphanum[idx] =
|
|
3281
3326
|
alphanumeric[drawPositiveDiscreteUniform(alphanumeric.length - 1)];
|
|
@@ -3364,9 +3409,7 @@ const sha256VerifyJSONResponse = (merchantKey, response, responseParams) => {
|
|
|
3364
3409
|
const signature = response.Ds_Signature;
|
|
3365
3410
|
// Base64url with padding, only substitutes + and /
|
|
3366
3411
|
const expSignature = sha256Sign(merchantKey, order, response.Ds_MerchantParameters);
|
|
3367
|
-
|
|
3368
|
-
if (!signature ||
|
|
3369
|
-
!Buffer.from(expSignature, 'base64').equals(base64url.toBuffer(signature))) {
|
|
3412
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3370
3413
|
throw new ParseError('Invalid signature');
|
|
3371
3414
|
}
|
|
3372
3415
|
};
|
|
@@ -3380,14 +3423,97 @@ const sha256SignJSONRequest = (merchantKey, serializedParams, requestParams) =>
|
|
|
3380
3423
|
};
|
|
3381
3424
|
};
|
|
3382
3425
|
|
|
3383
|
-
const
|
|
3426
|
+
const sha512SignatureVersions = ['HMAC_SHA512_V2', 'HMAC_SHA512_V1'];
|
|
3427
|
+
const sha512VerifyJSONResponse = (merchantKey, response, responseParams) => {
|
|
3428
|
+
if (!sha512SignatureVersions.includes(response.Ds_SignatureVersion)) {
|
|
3429
|
+
throw new RedsysError(`Unknown signature version: ${response.Ds_SignatureVersion}`);
|
|
3430
|
+
}
|
|
3431
|
+
const order = extractAndAssertOrderFromResponseParams(responseParams);
|
|
3432
|
+
const signature = response.Ds_Signature;
|
|
3433
|
+
// Signs the merchant parameters as received
|
|
3434
|
+
const expSignature = sha512Sign(merchantKey, order, response.Ds_MerchantParameters);
|
|
3435
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3436
|
+
throw new ParseError('Invalid signature');
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
const sha512V2SignJSONRequest = (merchantKey, serializedParams, requestParams) => {
|
|
3440
|
+
const order = extractAndAssertOrderFromRequestParams(requestParams);
|
|
3441
|
+
const signature = sha512Sign(merchantKey, order, serializedParams);
|
|
3442
|
+
return {
|
|
3443
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V2',
|
|
3444
|
+
Ds_MerchantParameters: serializedParams,
|
|
3445
|
+
Ds_Signature: signature
|
|
3446
|
+
};
|
|
3447
|
+
};
|
|
3448
|
+
const sha512V1SignJSONRequest = (merchantKey, serializedParams, requestParams) => {
|
|
3449
|
+
const order = extractAndAssertOrderFromRequestParams(requestParams);
|
|
3450
|
+
const signature = sha512Sign(merchantKey, order, serializedParams);
|
|
3451
|
+
return {
|
|
3452
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V1',
|
|
3453
|
+
Ds_MerchantParameters: serializedParams,
|
|
3454
|
+
Ds_Signature: Buffer.from(signature, 'base64url').toString('base64').replace(/\+/g, '-').replace(/\//g, '_')
|
|
3455
|
+
};
|
|
3456
|
+
};
|
|
3457
|
+
|
|
3458
|
+
const signatureVersions = ['HMAC_SHA512_V2', 'HMAC_SHA512_V1', 'HMAC_SHA256_V1'];
|
|
3459
|
+
|
|
3460
|
+
const assetIsSignatureVersion = (value) => {
|
|
3461
|
+
if (!signatureVersions.includes(value)) {
|
|
3462
|
+
throw new RedsysError(`Unknown signature version: ${String(value)}`);
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
const assetIsNonEmptyListOfSignatureVersions = (values) => {
|
|
3466
|
+
if (values.length < 1) {
|
|
3467
|
+
throw new RedsysError('List of signature versions is empty');
|
|
3468
|
+
}
|
|
3469
|
+
if (!values.every(sigItem => signatureVersions.includes(sigItem))) {
|
|
3470
|
+
throw new RedsysError(`Unknown signature versions in: ${values.map(String).join(',')}`);
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
const deserializeAndVerifyJSONResponse = (merchantKey, response, signatureVersions) => {
|
|
3384
3475
|
const params = deserializeJSONMerchantParams(response.Ds_MerchantParameters);
|
|
3385
|
-
|
|
3386
|
-
|
|
3476
|
+
assetIsSignatureVersion(response.Ds_SignatureVersion);
|
|
3477
|
+
if (!signatureVersions.includes(response.Ds_SignatureVersion)) {
|
|
3478
|
+
throw new RedsysError(`Signature version not allowed: ${response.Ds_SignatureVersion}`);
|
|
3479
|
+
}
|
|
3480
|
+
// Verify with the version declared by the response
|
|
3481
|
+
switch (response.Ds_SignatureVersion) {
|
|
3482
|
+
case 'HMAC_SHA512_V2': {
|
|
3483
|
+
sha512VerifyJSONResponse(merchantKey, response, params);
|
|
3484
|
+
return params;
|
|
3485
|
+
}
|
|
3486
|
+
case 'HMAC_SHA512_V1': {
|
|
3487
|
+
sha512VerifyJSONResponse(merchantKey, response, params);
|
|
3488
|
+
return params;
|
|
3489
|
+
}
|
|
3490
|
+
case 'HMAC_SHA256_V1': {
|
|
3491
|
+
sha256VerifyJSONResponse(merchantKey, response, params);
|
|
3492
|
+
return params;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
throw new RedsysError(
|
|
3496
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
3497
|
+
`Signature version not implemented: ${response.Ds_SignatureVersion}`);
|
|
3387
3498
|
};
|
|
3388
|
-
const serializeAndSignJSONRequest = (merchantKey, requestParams) => {
|
|
3389
|
-
|
|
3390
|
-
|
|
3499
|
+
const serializeAndSignJSONRequest = (merchantKey, requestParams, signatureVersion) => {
|
|
3500
|
+
switch (signatureVersion) {
|
|
3501
|
+
case 'HMAC_SHA512_V2': {
|
|
3502
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64url');
|
|
3503
|
+
return sha512V2SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3504
|
+
}
|
|
3505
|
+
case 'HMAC_SHA512_V1': {
|
|
3506
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64url');
|
|
3507
|
+
return sha512V1SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3508
|
+
}
|
|
3509
|
+
case 'HMAC_SHA256_V1': {
|
|
3510
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64');
|
|
3511
|
+
return sha256SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
throw new RedsysError(
|
|
3515
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
3516
|
+
`Signature version not implemented: ${signatureVersion}`);
|
|
3391
3517
|
};
|
|
3392
3518
|
|
|
3393
3519
|
/**
|
|
@@ -3431,7 +3557,7 @@ const verifySoapNotification = (merchantKey, xml, msg) => {
|
|
|
3431
3557
|
throw new ParseError('Invalid notification response');
|
|
3432
3558
|
}
|
|
3433
3559
|
const expSignature = sha256Sign(merchantKey, order, signedStr);
|
|
3434
|
-
if (!signature || signature
|
|
3560
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3435
3561
|
throw new ParseError('Invalid signature', signature, xml);
|
|
3436
3562
|
}
|
|
3437
3563
|
};
|
|
@@ -3451,8 +3577,8 @@ const deserializeAndVerifySoapNotification = (merchantKey, xml) => {
|
|
|
3451
3577
|
return msg.Request;
|
|
3452
3578
|
};
|
|
3453
3579
|
|
|
3454
|
-
const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams }) => {
|
|
3455
|
-
const payload = serializeAndSignJSONRequest(merchantKey, rawRequestParams);
|
|
3580
|
+
const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams, signatureVersions }) => {
|
|
3581
|
+
const payload = serializeAndSignJSONRequest(merchantKey, rawRequestParams, signatureVersions[0]);
|
|
3456
3582
|
const response = await fetch(url, {
|
|
3457
3583
|
method: 'POST',
|
|
3458
3584
|
headers: {
|
|
@@ -3474,7 +3600,7 @@ const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams }) => {
|
|
|
3474
3600
|
response: responseData
|
|
3475
3601
|
});
|
|
3476
3602
|
}
|
|
3477
|
-
return deserializeAndVerifyJSONResponse(merchantKey, responseData);
|
|
3603
|
+
return deserializeAndVerifyJSONResponse(merchantKey, responseData, signatureVersions);
|
|
3478
3604
|
};
|
|
3479
3605
|
|
|
3480
3606
|
/**
|
|
@@ -3506,7 +3632,7 @@ const SANDBOX_URLS = {
|
|
|
3506
3632
|
* @public
|
|
3507
3633
|
*/
|
|
3508
3634
|
const createRedsysAPI = (config) => {
|
|
3509
|
-
var _a;
|
|
3635
|
+
var _a, _b;
|
|
3510
3636
|
const fetch$1 = (_a = config.fetch) !== null && _a !== void 0 ? _a : fetch;
|
|
3511
3637
|
if (!config.secretKey || typeof config.secretKey !== 'string') {
|
|
3512
3638
|
throw new RedsysError('A secretKey key must be provided');
|
|
@@ -3519,6 +3645,8 @@ const createRedsysAPI = (config) => {
|
|
|
3519
3645
|
!config.urls.redirect) {
|
|
3520
3646
|
throw new RedsysError('URLs must be provided');
|
|
3521
3647
|
}
|
|
3648
|
+
const signatureVersions = (_b = config.signatureVersions) !== null && _b !== void 0 ? _b : ['HMAC_SHA512_V2', 'HMAC_SHA512_V1', 'HMAC_SHA256_V1'];
|
|
3649
|
+
assetIsNonEmptyListOfSignatureVersions(signatureVersions);
|
|
3522
3650
|
/**
|
|
3523
3651
|
* Sends a iniciaPeticion request using REST interface
|
|
3524
3652
|
*/
|
|
@@ -3526,7 +3654,8 @@ const createRedsysAPI = (config) => {
|
|
|
3526
3654
|
fetch: fetch$1,
|
|
3527
3655
|
url: config.urls.restIniciaPeticion,
|
|
3528
3656
|
merchantKey: config.secretKey,
|
|
3529
|
-
rawRequestParams: paramsInput
|
|
3657
|
+
rawRequestParams: paramsInput,
|
|
3658
|
+
signatureVersions
|
|
3530
3659
|
});
|
|
3531
3660
|
/**
|
|
3532
3661
|
* Sends a trataPeticion request using REST interface
|
|
@@ -3536,7 +3665,8 @@ const createRedsysAPI = (config) => {
|
|
|
3536
3665
|
fetch: fetch$1,
|
|
3537
3666
|
url: config.urls.restTrataPeticion,
|
|
3538
3667
|
merchantKey: config.secretKey,
|
|
3539
|
-
rawRequestParams: paramsInput
|
|
3668
|
+
rawRequestParams: paramsInput,
|
|
3669
|
+
signatureVersions
|
|
3540
3670
|
});
|
|
3541
3671
|
return result;
|
|
3542
3672
|
};
|
|
@@ -3544,7 +3674,7 @@ const createRedsysAPI = (config) => {
|
|
|
3544
3674
|
* Creates the parameters needed for a redirect form
|
|
3545
3675
|
*/
|
|
3546
3676
|
const createRedirectForm = (paramsInput) => {
|
|
3547
|
-
const body = serializeAndSignJSONRequest(config.secretKey, paramsInput);
|
|
3677
|
+
const body = serializeAndSignJSONRequest(config.secretKey, paramsInput, signatureVersions[0]);
|
|
3548
3678
|
return {
|
|
3549
3679
|
url: config.urls.redirect,
|
|
3550
3680
|
body
|
|
@@ -3557,7 +3687,7 @@ const createRedsysAPI = (config) => {
|
|
|
3557
3687
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3558
3688
|
body) => {
|
|
3559
3689
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3560
|
-
const params = deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3690
|
+
const params = deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3561
3691
|
return decodeUrlEntries(params);
|
|
3562
3692
|
};
|
|
3563
3693
|
/**
|
|
@@ -3567,7 +3697,7 @@ const createRedsysAPI = (config) => {
|
|
|
3567
3697
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3568
3698
|
body) =>
|
|
3569
3699
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3570
|
-
deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3700
|
+
deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3571
3701
|
/**
|
|
3572
3702
|
* Processes either a URLOK/URLKO query or a JSON REST notification
|
|
3573
3703
|
*
|
|
@@ -3583,19 +3713,23 @@ const createRedsysAPI = (config) => {
|
|
|
3583
3713
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3584
3714
|
body) => {
|
|
3585
3715
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3586
|
-
const params = deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3716
|
+
const params = deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3587
3717
|
return typeof params.Ds_Date === 'string' && params.Ds_Date.includes('%2F')
|
|
3588
3718
|
? decodeUrlEntries(params)
|
|
3589
3719
|
: params;
|
|
3590
3720
|
};
|
|
3591
3721
|
/**
|
|
3592
3722
|
* Parses and verifies the body of a SOAP notification
|
|
3723
|
+
*
|
|
3724
|
+
* @deprecated
|
|
3593
3725
|
*/
|
|
3594
3726
|
const processSoapNotification = (
|
|
3595
3727
|
/** SOAP notification as a XML string */
|
|
3596
3728
|
xml) => deserializeAndVerifySoapNotification(config.secretKey, xml);
|
|
3597
3729
|
/**
|
|
3598
3730
|
* Creates an answer for a SOAP notification, serializes and signs it
|
|
3731
|
+
*
|
|
3732
|
+
* @deprecated
|
|
3599
3733
|
*/
|
|
3600
3734
|
const createSoapNotificationAnswer = (
|
|
3601
3735
|
/** Order identifier */
|
|
@@ -3633,7 +3767,7 @@ const create3DSMethodForm = (emv3dsParams, notificationURL) => {
|
|
|
3633
3767
|
return {
|
|
3634
3768
|
url: emv3dsParams.threeDSMethodURL,
|
|
3635
3769
|
body: {
|
|
3636
|
-
threeDSMethodData:
|
|
3770
|
+
threeDSMethodData: Buffer.from(JSON.stringify(methodData), 'utf8').toString('base64url')
|
|
3637
3771
|
}
|
|
3638
3772
|
};
|
|
3639
3773
|
};
|
|
@@ -3668,7 +3802,7 @@ const create3DSv2ChallengeForm = (emv3dsV2Challenge) => ({
|
|
|
3668
3802
|
*/
|
|
3669
3803
|
const deserializeThreeDSMethodData = (threeDSMethodData) =>
|
|
3670
3804
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- assume it satisfies format
|
|
3671
|
-
JSON.parse(
|
|
3805
|
+
JSON.parse(Buffer.from(threeDSMethodData, 'base64url').toString('utf8'));
|
|
3672
3806
|
/**
|
|
3673
3807
|
* Deserialize `cres` field of a 3DS v2 challenge
|
|
3674
3808
|
*
|
|
@@ -3679,7 +3813,9 @@ const deserializeCres = (cres) =>
|
|
|
3679
3813
|
JSON.parse(Buffer.from(cres, 'base64').toString('utf8'));
|
|
3680
3814
|
|
|
3681
3815
|
const unescapeXML = (str) => {
|
|
3682
|
-
const xml = str.replace(
|
|
3816
|
+
const xml = str.replace(
|
|
3817
|
+
// eslint-disable-next-line prefer-named-capture-group
|
|
3818
|
+
/&(lt|#60|gt|#62|quot|#34|amp|#38|apos|#39);/g, (_match, p1) => {
|
|
3683
3819
|
switch (p1) {
|
|
3684
3820
|
case 'lt':
|
|
3685
3821
|
case '#60':
|
|
@@ -3756,6 +3892,7 @@ const detectSoapVersion = (req) => {
|
|
|
3756
3892
|
* @public
|
|
3757
3893
|
*/
|
|
3758
3894
|
const mimicSoapNotificationReceiver = (xml) => {
|
|
3895
|
+
// eslint-disable-next-line prefer-named-capture-group
|
|
3759
3896
|
const regex = /<(?:\w+:)?XML(?: [^<>]+)?>([^<>]+)<\/(?:\w+:)?XML>/;
|
|
3760
3897
|
const matchArray = regex.exec(xml);
|
|
3761
3898
|
const xmlContent = matchArray ? matchArray[1] : undefined;
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var undici = require('undici');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var crypto = require('crypto');
|
|
7
|
-
var node_crypto = require('node:crypto');
|
|
4
|
+
var node_http = require('node:http');
|
|
5
|
+
var crypto = require('node:crypto');
|
|
8
6
|
var fastXmlParser = require('fast-xml-parser');
|
|
9
7
|
var Decimal = require('decimal.js');
|
|
10
8
|
var dayjs = require('dayjs');
|
|
@@ -3026,7 +3024,7 @@ const getSISErrorCodeMessage = (code) => {
|
|
|
3026
3024
|
}
|
|
3027
3025
|
return SIS_ERROR_CODES[code.trim()];
|
|
3028
3026
|
};
|
|
3029
|
-
const getHTTPErrorCodeMessage = (code) =>
|
|
3027
|
+
const getHTTPErrorCodeMessage = (code) => node_http.STATUS_CODES[code];
|
|
3030
3028
|
|
|
3031
3029
|
/**
|
|
3032
3030
|
* Error superclass for all redsys-easy specific errors
|
|
@@ -3208,10 +3206,12 @@ const deserializeJSONMerchantParams = (strPayload) => {
|
|
|
3208
3206
|
}
|
|
3209
3207
|
return payload;
|
|
3210
3208
|
};
|
|
3211
|
-
const serializeJSONMerchantParams = (requestParams
|
|
3209
|
+
const serializeJSONMerchantParams = (requestParams,
|
|
3210
|
+
// HMAC_SHA512_V2 requires base64url without padding, redsys answers SIS0430 otherwise
|
|
3211
|
+
encoding) =>
|
|
3212
3212
|
// Official redsys docs escape "/" because they use php json_encode. I think
|
|
3213
3213
|
// this is wrong and it also works without escaping.
|
|
3214
|
-
Buffer.from(JSON.stringify(requestParams), 'utf8').toString(
|
|
3214
|
+
Buffer.from(JSON.stringify(requestParams), 'utf8').toString(encoding);
|
|
3215
3215
|
|
|
3216
3216
|
/**
|
|
3217
3217
|
* Adds padding to a buffer.
|
|
@@ -3234,6 +3234,7 @@ const zeroPad = (buf, blocksize) => {
|
|
|
3234
3234
|
const encrypt3DES = (key, message) => {
|
|
3235
3235
|
const keyBuf = Buffer.from(key, 'base64');
|
|
3236
3236
|
const iv = Buffer.alloc(8, 0);
|
|
3237
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
|
|
3237
3238
|
const messageBuf = Buffer.from(message.toString(), 'utf8');
|
|
3238
3239
|
// Align to blocksize by padding the message buffer
|
|
3239
3240
|
const paddedMessageBuf = zeroPad(messageBuf, 8);
|
|
@@ -3261,11 +3262,54 @@ const sha256Sign = (merchantKey, order, params) => {
|
|
|
3261
3262
|
.update(params)
|
|
3262
3263
|
.digest('base64');
|
|
3263
3264
|
};
|
|
3265
|
+
/**
|
|
3266
|
+
* Encrypt a message using AES-128-CBC
|
|
3267
|
+
*
|
|
3268
|
+
* @params key - Key to encrypt message, used as text
|
|
3269
|
+
* @params message - Message to be encrypted
|
|
3270
|
+
*/
|
|
3271
|
+
const encryptAES = (key, message) => {
|
|
3272
|
+
// Redsys takes the first 16 characters of the key, right padded with zeros
|
|
3273
|
+
const keyBuf = Buffer.from(key.slice(0, 16).padEnd(16, '0'), 'utf8');
|
|
3274
|
+
const iv = Buffer.alloc(16, 0);
|
|
3275
|
+
// PKCS#7 padding, the default
|
|
3276
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', keyBuf, iv);
|
|
3277
|
+
return Buffer.concat([cipher.update(message, 'utf8'), cipher.final()]);
|
|
3278
|
+
};
|
|
3279
|
+
/**
|
|
3280
|
+
* Compute HMAC_SHA512_V2 signature
|
|
3281
|
+
*
|
|
3282
|
+
* @params merchantKey - Key to encrypt message
|
|
3283
|
+
* @params order - Order number
|
|
3284
|
+
* @params params - Payload to sign
|
|
3285
|
+
*/
|
|
3286
|
+
const sha512Sign = (merchantKey, order, params) => {
|
|
3287
|
+
// The HMAC key is the encrypted order as a base64 string, not its bytes
|
|
3288
|
+
const orderKey = encryptAES(merchantKey, order).toString('base64');
|
|
3289
|
+
return crypto
|
|
3290
|
+
.createHmac('sha512', orderKey)
|
|
3291
|
+
.update(params)
|
|
3292
|
+
.digest('base64url');
|
|
3293
|
+
};
|
|
3294
|
+
/**
|
|
3295
|
+
* Compare two signatures in constant time
|
|
3296
|
+
*
|
|
3297
|
+
* @params expected - Signature computed locally, base64 or base64url encoded
|
|
3298
|
+
* @params received - Signature received from redsys, base64 or base64url encoded
|
|
3299
|
+
*/
|
|
3300
|
+
const isSignatureEqual = (expected, received) => {
|
|
3301
|
+
// Comparing different base64 encodings is messy. Make it foolproof by comparing buffers.
|
|
3302
|
+
// In node, the input encoding 'base64' accepts also base64url, with and without padding.
|
|
3303
|
+
const expectedBuf = Buffer.from(expected, 'base64');
|
|
3304
|
+
const receivedBuf = Buffer.from(received, 'base64');
|
|
3305
|
+
return (expectedBuf.length === receivedBuf.length &&
|
|
3306
|
+
crypto.timingSafeEqual(expectedBuf, receivedBuf));
|
|
3307
|
+
};
|
|
3264
3308
|
|
|
3265
3309
|
/**
|
|
3266
3310
|
* Discrete uniform distribution with domain (0 to *max*)
|
|
3267
3311
|
*/
|
|
3268
|
-
const drawPositiveDiscreteUniform = (max) =>
|
|
3312
|
+
const drawPositiveDiscreteUniform = (max) => crypto.randomInt(max + 1);
|
|
3269
3313
|
const alphanumeric = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
3270
3314
|
/**
|
|
3271
3315
|
* Generates a random order ID following redsys requirements
|
|
@@ -3278,6 +3322,7 @@ const randomTransactionId = () => {
|
|
|
3278
3322
|
const numStr = num.toString().padStart(4, '0');
|
|
3279
3323
|
// Random 8 letter alphanumeric sequence
|
|
3280
3324
|
const alphanum = new Array(8);
|
|
3325
|
+
// eslint-disable-next-line no-plusplus
|
|
3281
3326
|
for (let idx = 0; idx < 8; idx++) {
|
|
3282
3327
|
alphanum[idx] =
|
|
3283
3328
|
alphanumeric[drawPositiveDiscreteUniform(alphanumeric.length - 1)];
|
|
@@ -3366,9 +3411,7 @@ const sha256VerifyJSONResponse = (merchantKey, response, responseParams) => {
|
|
|
3366
3411
|
const signature = response.Ds_Signature;
|
|
3367
3412
|
// Base64url with padding, only substitutes + and /
|
|
3368
3413
|
const expSignature = sha256Sign(merchantKey, order, response.Ds_MerchantParameters);
|
|
3369
|
-
|
|
3370
|
-
if (!signature ||
|
|
3371
|
-
!Buffer.from(expSignature, 'base64').equals(base64url.toBuffer(signature))) {
|
|
3414
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3372
3415
|
throw new ParseError('Invalid signature');
|
|
3373
3416
|
}
|
|
3374
3417
|
};
|
|
@@ -3382,14 +3425,97 @@ const sha256SignJSONRequest = (merchantKey, serializedParams, requestParams) =>
|
|
|
3382
3425
|
};
|
|
3383
3426
|
};
|
|
3384
3427
|
|
|
3385
|
-
const
|
|
3428
|
+
const sha512SignatureVersions = ['HMAC_SHA512_V2', 'HMAC_SHA512_V1'];
|
|
3429
|
+
const sha512VerifyJSONResponse = (merchantKey, response, responseParams) => {
|
|
3430
|
+
if (!sha512SignatureVersions.includes(response.Ds_SignatureVersion)) {
|
|
3431
|
+
throw new RedsysError(`Unknown signature version: ${response.Ds_SignatureVersion}`);
|
|
3432
|
+
}
|
|
3433
|
+
const order = extractAndAssertOrderFromResponseParams(responseParams);
|
|
3434
|
+
const signature = response.Ds_Signature;
|
|
3435
|
+
// Signs the merchant parameters as received
|
|
3436
|
+
const expSignature = sha512Sign(merchantKey, order, response.Ds_MerchantParameters);
|
|
3437
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3438
|
+
throw new ParseError('Invalid signature');
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3441
|
+
const sha512V2SignJSONRequest = (merchantKey, serializedParams, requestParams) => {
|
|
3442
|
+
const order = extractAndAssertOrderFromRequestParams(requestParams);
|
|
3443
|
+
const signature = sha512Sign(merchantKey, order, serializedParams);
|
|
3444
|
+
return {
|
|
3445
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V2',
|
|
3446
|
+
Ds_MerchantParameters: serializedParams,
|
|
3447
|
+
Ds_Signature: signature
|
|
3448
|
+
};
|
|
3449
|
+
};
|
|
3450
|
+
const sha512V1SignJSONRequest = (merchantKey, serializedParams, requestParams) => {
|
|
3451
|
+
const order = extractAndAssertOrderFromRequestParams(requestParams);
|
|
3452
|
+
const signature = sha512Sign(merchantKey, order, serializedParams);
|
|
3453
|
+
return {
|
|
3454
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V1',
|
|
3455
|
+
Ds_MerchantParameters: serializedParams,
|
|
3456
|
+
Ds_Signature: Buffer.from(signature, 'base64url').toString('base64').replace(/\+/g, '-').replace(/\//g, '_')
|
|
3457
|
+
};
|
|
3458
|
+
};
|
|
3459
|
+
|
|
3460
|
+
const signatureVersions = ['HMAC_SHA512_V2', 'HMAC_SHA512_V1', 'HMAC_SHA256_V1'];
|
|
3461
|
+
|
|
3462
|
+
const assetIsSignatureVersion = (value) => {
|
|
3463
|
+
if (!signatureVersions.includes(value)) {
|
|
3464
|
+
throw new RedsysError(`Unknown signature version: ${String(value)}`);
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
const assetIsNonEmptyListOfSignatureVersions = (values) => {
|
|
3468
|
+
if (values.length < 1) {
|
|
3469
|
+
throw new RedsysError('List of signature versions is empty');
|
|
3470
|
+
}
|
|
3471
|
+
if (!values.every(sigItem => signatureVersions.includes(sigItem))) {
|
|
3472
|
+
throw new RedsysError(`Unknown signature versions in: ${values.map(String).join(',')}`);
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
|
|
3476
|
+
const deserializeAndVerifyJSONResponse = (merchantKey, response, signatureVersions) => {
|
|
3386
3477
|
const params = deserializeJSONMerchantParams(response.Ds_MerchantParameters);
|
|
3387
|
-
|
|
3388
|
-
|
|
3478
|
+
assetIsSignatureVersion(response.Ds_SignatureVersion);
|
|
3479
|
+
if (!signatureVersions.includes(response.Ds_SignatureVersion)) {
|
|
3480
|
+
throw new RedsysError(`Signature version not allowed: ${response.Ds_SignatureVersion}`);
|
|
3481
|
+
}
|
|
3482
|
+
// Verify with the version declared by the response
|
|
3483
|
+
switch (response.Ds_SignatureVersion) {
|
|
3484
|
+
case 'HMAC_SHA512_V2': {
|
|
3485
|
+
sha512VerifyJSONResponse(merchantKey, response, params);
|
|
3486
|
+
return params;
|
|
3487
|
+
}
|
|
3488
|
+
case 'HMAC_SHA512_V1': {
|
|
3489
|
+
sha512VerifyJSONResponse(merchantKey, response, params);
|
|
3490
|
+
return params;
|
|
3491
|
+
}
|
|
3492
|
+
case 'HMAC_SHA256_V1': {
|
|
3493
|
+
sha256VerifyJSONResponse(merchantKey, response, params);
|
|
3494
|
+
return params;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
throw new RedsysError(
|
|
3498
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
3499
|
+
`Signature version not implemented: ${response.Ds_SignatureVersion}`);
|
|
3389
3500
|
};
|
|
3390
|
-
const serializeAndSignJSONRequest = (merchantKey, requestParams) => {
|
|
3391
|
-
|
|
3392
|
-
|
|
3501
|
+
const serializeAndSignJSONRequest = (merchantKey, requestParams, signatureVersion) => {
|
|
3502
|
+
switch (signatureVersion) {
|
|
3503
|
+
case 'HMAC_SHA512_V2': {
|
|
3504
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64url');
|
|
3505
|
+
return sha512V2SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3506
|
+
}
|
|
3507
|
+
case 'HMAC_SHA512_V1': {
|
|
3508
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64url');
|
|
3509
|
+
return sha512V1SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3510
|
+
}
|
|
3511
|
+
case 'HMAC_SHA256_V1': {
|
|
3512
|
+
const serializedParams = serializeJSONMerchantParams(requestParams, 'base64');
|
|
3513
|
+
return sha256SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
throw new RedsysError(
|
|
3517
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
3518
|
+
`Signature version not implemented: ${signatureVersion}`);
|
|
3393
3519
|
};
|
|
3394
3520
|
|
|
3395
3521
|
/**
|
|
@@ -3433,7 +3559,7 @@ const verifySoapNotification = (merchantKey, xml, msg) => {
|
|
|
3433
3559
|
throw new ParseError('Invalid notification response');
|
|
3434
3560
|
}
|
|
3435
3561
|
const expSignature = sha256Sign(merchantKey, order, signedStr);
|
|
3436
|
-
if (!signature || signature
|
|
3562
|
+
if (!signature || !isSignatureEqual(expSignature, signature)) {
|
|
3437
3563
|
throw new ParseError('Invalid signature', signature, xml);
|
|
3438
3564
|
}
|
|
3439
3565
|
};
|
|
@@ -3453,8 +3579,8 @@ const deserializeAndVerifySoapNotification = (merchantKey, xml) => {
|
|
|
3453
3579
|
return msg.Request;
|
|
3454
3580
|
};
|
|
3455
3581
|
|
|
3456
|
-
const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams }) => {
|
|
3457
|
-
const payload = serializeAndSignJSONRequest(merchantKey, rawRequestParams);
|
|
3582
|
+
const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams, signatureVersions }) => {
|
|
3583
|
+
const payload = serializeAndSignJSONRequest(merchantKey, rawRequestParams, signatureVersions[0]);
|
|
3458
3584
|
const response = await fetch(url, {
|
|
3459
3585
|
method: 'POST',
|
|
3460
3586
|
headers: {
|
|
@@ -3476,7 +3602,7 @@ const jsonRequest = async ({ fetch, url, merchantKey, rawRequestParams }) => {
|
|
|
3476
3602
|
response: responseData
|
|
3477
3603
|
});
|
|
3478
3604
|
}
|
|
3479
|
-
return deserializeAndVerifyJSONResponse(merchantKey, responseData);
|
|
3605
|
+
return deserializeAndVerifyJSONResponse(merchantKey, responseData, signatureVersions);
|
|
3480
3606
|
};
|
|
3481
3607
|
|
|
3482
3608
|
/**
|
|
@@ -3508,7 +3634,7 @@ const SANDBOX_URLS = {
|
|
|
3508
3634
|
* @public
|
|
3509
3635
|
*/
|
|
3510
3636
|
const createRedsysAPI = (config) => {
|
|
3511
|
-
var _a;
|
|
3637
|
+
var _a, _b;
|
|
3512
3638
|
const fetch = (_a = config.fetch) !== null && _a !== void 0 ? _a : undici.fetch;
|
|
3513
3639
|
if (!config.secretKey || typeof config.secretKey !== 'string') {
|
|
3514
3640
|
throw new RedsysError('A secretKey key must be provided');
|
|
@@ -3521,6 +3647,8 @@ const createRedsysAPI = (config) => {
|
|
|
3521
3647
|
!config.urls.redirect) {
|
|
3522
3648
|
throw new RedsysError('URLs must be provided');
|
|
3523
3649
|
}
|
|
3650
|
+
const signatureVersions = (_b = config.signatureVersions) !== null && _b !== void 0 ? _b : ['HMAC_SHA512_V2', 'HMAC_SHA512_V1', 'HMAC_SHA256_V1'];
|
|
3651
|
+
assetIsNonEmptyListOfSignatureVersions(signatureVersions);
|
|
3524
3652
|
/**
|
|
3525
3653
|
* Sends a iniciaPeticion request using REST interface
|
|
3526
3654
|
*/
|
|
@@ -3528,7 +3656,8 @@ const createRedsysAPI = (config) => {
|
|
|
3528
3656
|
fetch,
|
|
3529
3657
|
url: config.urls.restIniciaPeticion,
|
|
3530
3658
|
merchantKey: config.secretKey,
|
|
3531
|
-
rawRequestParams: paramsInput
|
|
3659
|
+
rawRequestParams: paramsInput,
|
|
3660
|
+
signatureVersions
|
|
3532
3661
|
});
|
|
3533
3662
|
/**
|
|
3534
3663
|
* Sends a trataPeticion request using REST interface
|
|
@@ -3538,7 +3667,8 @@ const createRedsysAPI = (config) => {
|
|
|
3538
3667
|
fetch,
|
|
3539
3668
|
url: config.urls.restTrataPeticion,
|
|
3540
3669
|
merchantKey: config.secretKey,
|
|
3541
|
-
rawRequestParams: paramsInput
|
|
3670
|
+
rawRequestParams: paramsInput,
|
|
3671
|
+
signatureVersions
|
|
3542
3672
|
});
|
|
3543
3673
|
return result;
|
|
3544
3674
|
};
|
|
@@ -3546,7 +3676,7 @@ const createRedsysAPI = (config) => {
|
|
|
3546
3676
|
* Creates the parameters needed for a redirect form
|
|
3547
3677
|
*/
|
|
3548
3678
|
const createRedirectForm = (paramsInput) => {
|
|
3549
|
-
const body = serializeAndSignJSONRequest(config.secretKey, paramsInput);
|
|
3679
|
+
const body = serializeAndSignJSONRequest(config.secretKey, paramsInput, signatureVersions[0]);
|
|
3550
3680
|
return {
|
|
3551
3681
|
url: config.urls.redirect,
|
|
3552
3682
|
body
|
|
@@ -3559,7 +3689,7 @@ const createRedsysAPI = (config) => {
|
|
|
3559
3689
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3560
3690
|
body) => {
|
|
3561
3691
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3562
|
-
const params = deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3692
|
+
const params = deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3563
3693
|
return decodeUrlEntries(params);
|
|
3564
3694
|
};
|
|
3565
3695
|
/**
|
|
@@ -3569,7 +3699,7 @@ const createRedsysAPI = (config) => {
|
|
|
3569
3699
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3570
3700
|
body) =>
|
|
3571
3701
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3572
|
-
deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3702
|
+
deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3573
3703
|
/**
|
|
3574
3704
|
* Processes either a URLOK/URLKO query or a JSON REST notification
|
|
3575
3705
|
*
|
|
@@ -3585,19 +3715,23 @@ const createRedsysAPI = (config) => {
|
|
|
3585
3715
|
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
3586
3716
|
body) => {
|
|
3587
3717
|
// A notification can't contain a gateway error, it didn't initiate the request
|
|
3588
|
-
const params = deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
3718
|
+
const params = deserializeAndVerifyJSONResponse(config.secretKey, body, signatureVersions);
|
|
3589
3719
|
return typeof params.Ds_Date === 'string' && params.Ds_Date.includes('%2F')
|
|
3590
3720
|
? decodeUrlEntries(params)
|
|
3591
3721
|
: params;
|
|
3592
3722
|
};
|
|
3593
3723
|
/**
|
|
3594
3724
|
* Parses and verifies the body of a SOAP notification
|
|
3725
|
+
*
|
|
3726
|
+
* @deprecated
|
|
3595
3727
|
*/
|
|
3596
3728
|
const processSoapNotification = (
|
|
3597
3729
|
/** SOAP notification as a XML string */
|
|
3598
3730
|
xml) => deserializeAndVerifySoapNotification(config.secretKey, xml);
|
|
3599
3731
|
/**
|
|
3600
3732
|
* Creates an answer for a SOAP notification, serializes and signs it
|
|
3733
|
+
*
|
|
3734
|
+
* @deprecated
|
|
3601
3735
|
*/
|
|
3602
3736
|
const createSoapNotificationAnswer = (
|
|
3603
3737
|
/** Order identifier */
|
|
@@ -3635,7 +3769,7 @@ const create3DSMethodForm = (emv3dsParams, notificationURL) => {
|
|
|
3635
3769
|
return {
|
|
3636
3770
|
url: emv3dsParams.threeDSMethodURL,
|
|
3637
3771
|
body: {
|
|
3638
|
-
threeDSMethodData:
|
|
3772
|
+
threeDSMethodData: Buffer.from(JSON.stringify(methodData), 'utf8').toString('base64url')
|
|
3639
3773
|
}
|
|
3640
3774
|
};
|
|
3641
3775
|
};
|
|
@@ -3670,7 +3804,7 @@ const create3DSv2ChallengeForm = (emv3dsV2Challenge) => ({
|
|
|
3670
3804
|
*/
|
|
3671
3805
|
const deserializeThreeDSMethodData = (threeDSMethodData) =>
|
|
3672
3806
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- assume it satisfies format
|
|
3673
|
-
JSON.parse(
|
|
3807
|
+
JSON.parse(Buffer.from(threeDSMethodData, 'base64url').toString('utf8'));
|
|
3674
3808
|
/**
|
|
3675
3809
|
* Deserialize `cres` field of a 3DS v2 challenge
|
|
3676
3810
|
*
|
|
@@ -3681,7 +3815,9 @@ const deserializeCres = (cres) =>
|
|
|
3681
3815
|
JSON.parse(Buffer.from(cres, 'base64').toString('utf8'));
|
|
3682
3816
|
|
|
3683
3817
|
const unescapeXML = (str) => {
|
|
3684
|
-
const xml = str.replace(
|
|
3818
|
+
const xml = str.replace(
|
|
3819
|
+
// eslint-disable-next-line prefer-named-capture-group
|
|
3820
|
+
/&(lt|#60|gt|#62|quot|#34|amp|#38|apos|#39);/g, (_match, p1) => {
|
|
3685
3821
|
switch (p1) {
|
|
3686
3822
|
case 'lt':
|
|
3687
3823
|
case '#60':
|
|
@@ -3758,6 +3894,7 @@ const detectSoapVersion = (req) => {
|
|
|
3758
3894
|
* @public
|
|
3759
3895
|
*/
|
|
3760
3896
|
const mimicSoapNotificationReceiver = (xml) => {
|
|
3897
|
+
// eslint-disable-next-line prefer-named-capture-group
|
|
3761
3898
|
const regex = /<(?:\w+:)?XML(?: [^<>]+)?>([^<>]+)<\/(?:\w+:)?XML>/;
|
|
3762
3899
|
const matchArray = regex.exec(xml);
|
|
3763
3900
|
const xmlContent = matchArray ? matchArray[1] : undefined;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const deserializeJSONMerchantParams: <DeserializedResponseParams = unknown>(strPayload: string) => DeserializedResponseParams;
|
|
2
|
-
export declare const serializeJSONMerchantParams: (requestParams: unknown) => string;
|
|
2
|
+
export declare const serializeJSONMerchantParams: (requestParams: unknown, encoding: "base64" | "base64url") => string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResponseJSONSuccess, CommonRawRequestParams, CommonRawResponseParams, SHA512V1SignedJSONParameters, SHA512V2SignedJSONParameters } from '../types/api';
|
|
2
|
+
export declare const sha512VerifyJSONResponse: (merchantKey: string, response: ResponseJSONSuccess, responseParams: CommonRawResponseParams) => void;
|
|
3
|
+
export declare const sha512V2SignJSONRequest: (merchantKey: string, serializedParams: string, requestParams: CommonRawRequestParams) => SHA512V2SignedJSONParameters;
|
|
4
|
+
export declare const sha512V1SignJSONRequest: (merchantKey: string, serializedParams: string, requestParams: CommonRawRequestParams) => SHA512V1SignedJSONParameters;
|
package/lib/rest/json.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ResponseJSONSuccess, CommonRawRequestParams, CommonRawResponseParams,
|
|
2
|
-
export declare const deserializeAndVerifyJSONResponse: <ResponseParams extends CommonRawResponseParams>(merchantKey: string, response: ResponseJSONSuccess) => ResponseParams;
|
|
3
|
-
export declare const serializeAndSignJSONRequest: <RequestParams extends CommonRawRequestParams>(merchantKey: string, requestParams: RequestParams) =>
|
|
1
|
+
import type { ResponseJSONSuccess, CommonRawRequestParams, CommonRawResponseParams, SignedJSONParameters, SignatureVersion } from '../types/api';
|
|
2
|
+
export declare const deserializeAndVerifyJSONResponse: <ResponseParams extends CommonRawResponseParams>(merchantKey: string, response: ResponseJSONSuccess, signatureVersions: readonly SignatureVersion[]) => ResponseParams;
|
|
3
|
+
export declare const serializeAndSignJSONRequest: <RequestParams extends CommonRawRequestParams>(merchantKey: string, requestParams: RequestParams, signatureVersion: SignatureVersion) => SignedJSONParameters;
|
package/lib/rest/requests.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { fetch as Fetch } from 'undici';
|
|
2
|
-
import type { CommonRawRequestParams, CommonRawResponseParams } from '../types/api';
|
|
3
|
-
export declare const jsonRequest: <RequestParams extends CommonRawRequestParams, ResponseParams extends CommonRawResponseParams>({ fetch, url, merchantKey, rawRequestParams }: {
|
|
2
|
+
import type { CommonRawRequestParams, CommonRawResponseParams, SignatureVersion } from '../types/api';
|
|
3
|
+
export declare const jsonRequest: <RequestParams extends CommonRawRequestParams, ResponseParams extends CommonRawResponseParams>({ fetch, url, merchantKey, rawRequestParams, signatureVersions }: {
|
|
4
4
|
fetch: typeof Fetch;
|
|
5
5
|
url: string;
|
|
6
6
|
merchantKey: string;
|
|
7
7
|
rawRequestParams: RequestParams;
|
|
8
|
+
signatureVersions: readonly [SignatureVersion, ...SignatureVersion[]];
|
|
8
9
|
}) => Promise<ResponseParams>;
|
package/lib/types/api.d.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface ResponseJSONError {
|
|
|
57
57
|
* Body of a redsys JSON HTTP response
|
|
58
58
|
*/
|
|
59
59
|
export type ResponseJSON = ResponseJSONSuccess | ResponseJSONError;
|
|
60
|
+
/**
|
|
61
|
+
* Signature versions supported by redsys-easy
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type SignatureVersion = 'HMAC_SHA256_V1' | 'HMAC_SHA512_V1' | 'HMAC_SHA512_V2';
|
|
60
66
|
/**
|
|
61
67
|
* SHA256 signed JSON request parameters
|
|
62
68
|
*/
|
|
@@ -65,6 +71,26 @@ export interface SHA256SignedJSONParameters {
|
|
|
65
71
|
Ds_MerchantParameters: string;
|
|
66
72
|
Ds_Signature: string;
|
|
67
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* HMAC_SHA512_V1 signed JSON request parameters
|
|
76
|
+
*/
|
|
77
|
+
export interface SHA512V1SignedJSONParameters {
|
|
78
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V1';
|
|
79
|
+
Ds_MerchantParameters: string;
|
|
80
|
+
Ds_Signature: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* HMAC_SHA512_V2 signed JSON request parameters
|
|
84
|
+
*/
|
|
85
|
+
export interface SHA512V2SignedJSONParameters {
|
|
86
|
+
Ds_SignatureVersion: 'HMAC_SHA512_V2';
|
|
87
|
+
Ds_MerchantParameters: string;
|
|
88
|
+
Ds_Signature: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Signed JSON request parameters
|
|
92
|
+
*/
|
|
93
|
+
export type SignedJSONParameters = SHA256SignedJSONParameters | SHA512V1SignedJSONParameters | SHA512V2SignedJSONParameters;
|
|
68
94
|
export interface SoapNotificationResponse {
|
|
69
95
|
order: string;
|
|
70
96
|
allow: boolean;
|
|
@@ -76,5 +102,5 @@ export interface SoapNotificationResponse {
|
|
|
76
102
|
*/
|
|
77
103
|
export interface RedirectForm {
|
|
78
104
|
url: string;
|
|
79
|
-
body:
|
|
105
|
+
body: SignedJSONParameters;
|
|
80
106
|
}
|
|
@@ -61,6 +61,14 @@ export interface BaseInputParams {
|
|
|
61
61
|
DS_MERCHANT_COF_TXNID?: string | undefined;
|
|
62
62
|
/** Credential On File transaction type */
|
|
63
63
|
DS_MERCHANT_COF_TYPE?: 'I' | 'R' | 'H' | 'E' | 'D' | 'M' | 'N' | 'C' | undefined;
|
|
64
|
+
/** xPay data in base64 format, mutually exclusive with DS_XPAYDECODEDDATA */
|
|
65
|
+
DS_XPAYDATA?: string | undefined;
|
|
66
|
+
/** xPay data in json format, mutually exclusive with DS_XPAYDATA */
|
|
67
|
+
DS_XPAYDECODEDDATA?: Record<string, unknown> | undefined;
|
|
68
|
+
/** xPay origin */
|
|
69
|
+
DS_XPAYORIGEN?: 'WEB' | 'InApp' | undefined;
|
|
70
|
+
/** xPay type */
|
|
71
|
+
DS_XPAYTYPE?: 'Google' | 'Apple' | undefined;
|
|
64
72
|
}
|
|
65
73
|
/**
|
|
66
74
|
* Input parameters for a redsys redirect request
|
|
@@ -91,14 +99,6 @@ export interface RedirectInputParams extends BaseInputParams {
|
|
|
91
99
|
* @public
|
|
92
100
|
*/
|
|
93
101
|
export interface RequestInputParams extends BaseInputParams, Pick<RedirectInputParams, 'DS_MERCHANT_MERCHANTURL' | 'DS_MERCHANT_URLOK' | 'DS_MERCHANT_URLKO' | 'DS_MERCHANT_CONSUMERLANGUAGE' | 'DS_MERCHANT_PAYMETHODS' | 'DS_MERCHANT_PERSOCODE' | 'DS_MERCHANT_SHIPPINGADDRESSPYP'> {
|
|
94
|
-
/** xPay data in base64 format, mutually exclusive with DS_XPAYDECODEDDATA */
|
|
95
|
-
DS_XPAYDATA?: string | undefined;
|
|
96
|
-
/** xPay data in json format, mutually exclusive with DS_XPAYDATA */
|
|
97
|
-
DS_XPAYDECODEDDATA?: Record<string, unknown> | undefined;
|
|
98
|
-
/** xPay origin */
|
|
99
|
-
DS_XPAYORIGEN?: 'WEB' | 'InApp' | undefined;
|
|
100
|
-
/** xPay type */
|
|
101
|
-
DS_XPAYTYPE?: 'Google' | 'Apple' | undefined;
|
|
102
102
|
/** PUC identifier, number */
|
|
103
103
|
DS_ACQUIRER_IDENTIFIER?: string | undefined;
|
|
104
104
|
/** Matching data */
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SignatureVersion } from '../types/api';
|
|
2
|
+
export declare const assetIsSignatureVersion: (value: unknown) => asserts value is SignatureVersion;
|
|
3
|
+
export declare const assetIsNonEmptyListOfSignatureVersions: (values: readonly unknown[]) => asserts values is readonly [SignatureVersion, ...SignatureVersion[]];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redsys-easy",
|
|
3
3
|
"description": "Node JS interface for Redsys payment gateway",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0",
|
|
5
5
|
"author": "Javier Garcia <javiertury@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -40,80 +40,53 @@
|
|
|
40
40
|
"commit-msg": "pnpm exec commitlint -e"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"base64url": "^3.0.1",
|
|
44
43
|
"dayjs": "^1.11.9",
|
|
45
44
|
"decimal.js": "^10.4.3",
|
|
46
45
|
"fast-xml-parser": "^5.0.6",
|
|
47
46
|
"tslib": "^2.6.1",
|
|
48
|
-
"undici": "^
|
|
47
|
+
"undici": "^8.0.1",
|
|
48
|
+
"vitest": "^5.1.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@commitlint/cli": "^
|
|
51
|
+
"@commitlint/cli": "^20.5.0",
|
|
52
52
|
"@commitlint/config-conventional": "^17.6.7",
|
|
53
53
|
"@koa/router": "^12.0.0",
|
|
54
|
-
"@microsoft/api-documenter": "^7.
|
|
55
|
-
"@microsoft/api-extractor": "^7.
|
|
54
|
+
"@microsoft/api-documenter": "^7.29.7",
|
|
55
|
+
"@microsoft/api-extractor": "^7.57.7",
|
|
56
56
|
"@rollup/plugin-node-resolve": "^15.1.0",
|
|
57
57
|
"@types/cheerio": "^0.22.31",
|
|
58
|
-
"@types/jest": "^30.0.0",
|
|
59
58
|
"@types/koa": "^3.0.0",
|
|
60
59
|
"@types/koa-bodyparser": "^4.3.10",
|
|
61
60
|
"@types/koa__router": "^12.0.0",
|
|
62
61
|
"cheerio": "1.0.0-rc.12",
|
|
63
|
-
"commit-and-tag-version": "^12.
|
|
64
|
-
"eslint": "^
|
|
65
|
-
"eslint-config-love": "^
|
|
66
|
-
"eslint-plugin-jest": "^28.14.0",
|
|
67
|
-
"jest": "^30.2.0",
|
|
62
|
+
"commit-and-tag-version": "^12.7.1",
|
|
63
|
+
"eslint": "^10.1.0",
|
|
64
|
+
"eslint-config-love": "^151.0.0",
|
|
68
65
|
"koa": "^3.0.0",
|
|
69
66
|
"koa-bodyparser": "^4.4.1",
|
|
70
67
|
"lint-staged": "^15.3.0",
|
|
71
68
|
"markdown-magic": "^4.0.4",
|
|
72
|
-
"rimraf": "^6.1.
|
|
69
|
+
"rimraf": "^6.1.3",
|
|
73
70
|
"rollup": "^3.29.5",
|
|
74
|
-
"rollup-plugin-typescript2": "^0.
|
|
71
|
+
"rollup-plugin-typescript2": "^0.37.0",
|
|
75
72
|
"simple-git-hooks": "^2.9.0",
|
|
76
73
|
"ts-expect": "^1.3.0",
|
|
77
|
-
"ts-jest": "^29.4.5",
|
|
78
74
|
"typescript": "^5.1.6"
|
|
79
75
|
},
|
|
80
|
-
"jest": {
|
|
81
|
-
"preset": "ts-jest/presets/js-with-ts",
|
|
82
|
-
"testEnvironment": "node",
|
|
83
|
-
"testMatch": [
|
|
84
|
-
"<rootDir>/test/unit/**/*.test.ts",
|
|
85
|
-
"<rootDir>/test/integration/**/*.test.ts",
|
|
86
|
-
"<rootDir>/src/**/*.test.ts"
|
|
87
|
-
],
|
|
88
|
-
"setupFilesAfterEnv": [
|
|
89
|
-
"./test/jest.setup.ts"
|
|
90
|
-
],
|
|
91
|
-
"transform": {
|
|
92
|
-
"^.+\\.tsx?$": [
|
|
93
|
-
"ts-jest",
|
|
94
|
-
{
|
|
95
|
-
"tsconfig": "tsconfig.test.json"
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
"moduleNameMapper": {
|
|
100
|
-
"redsys-easy": "<rootDir>/src"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
76
|
"scripts": {
|
|
104
77
|
"lint": "eslint .",
|
|
105
78
|
"build:clean": "rimraf lib",
|
|
106
79
|
"build:code": "rollup -c rollup.config.mjs",
|
|
107
80
|
"build:api": "api-extractor run --local",
|
|
108
81
|
"build": "pnpm run build:clean && pnpm run lint && pnpm run build:code && pnpm run build:api",
|
|
109
|
-
"test:unit": "
|
|
110
|
-
"test:integration": "
|
|
111
|
-
"test:code": "
|
|
82
|
+
"test:unit": "vitest run ./src/*.test.ts ./src/**/*.test.ts",
|
|
83
|
+
"test:integration": "vitest run ./test/integration/**/*.test.ts",
|
|
84
|
+
"test:code": "vitest run",
|
|
112
85
|
"test:readme": "tsc -p ./tsconfig.readme.json",
|
|
113
86
|
"test": "pnpm run test:code && pnpm run test:readme",
|
|
114
87
|
"docs:api": "api-documenter markdown --input-folder ./api/temp --output-folder ./docs",
|
|
115
88
|
"docs:readme": "md-magic --files ./README.md",
|
|
116
89
|
"docs": "pnpm run docs:api && pnpm run docs:readme",
|
|
117
|
-
"release": "pnpm run test && commit-and-tag-version --dry-run && pnpm run docs && git add docs CHANGELOG.md && commit-and-tag-version --commit-all"
|
|
90
|
+
"release": "pnpm run test:unit && commit-and-tag-version --dry-run && pnpm run docs && git add docs CHANGELOG.md && commit-and-tag-version --commit-all"
|
|
118
91
|
}
|
|
119
92
|
}
|