redsys-easy 5.3.2 → 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 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
@@ -0,0 +1,2 @@
1
+ import type { SignatureVersion } from '../types/api';
2
+ export declare const signatureVersions: readonly SignatureVersion[];
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 base64url from 'base64url';
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('base64');
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
- // Comparing different base64 encodings is messy. Make it foolproof by comparing buffers.
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 deserializeAndVerifyJSONResponse = (merchantKey, response) => {
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
- sha256VerifyJSONResponse(merchantKey, response, params);
3386
- return params;
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
- const serializedParams = serializeJSONMerchantParams(requestParams);
3390
- return sha256SignJSONRequest(merchantKey, serializedParams, requestParams);
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 !== expSignature) {
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: base64url.encode(JSON.stringify(methodData))
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(base64url.decode(threeDSMethodData));
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(/&(lt|#60|gt|#62|quot|#34|amp|#38|apos|#39);/g, (_match, p1) => {
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 http = require('http');
5
- var base64url = require('base64url');
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) => http.STATUS_CODES[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('base64');
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) => node_crypto.randomInt(max + 1);
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
- // Comparing different base64 encodings is messy. Make it foolproof by comparing buffers.
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 deserializeAndVerifyJSONResponse = (merchantKey, response) => {
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
- sha256VerifyJSONResponse(merchantKey, response, params);
3388
- return params;
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
- const serializedParams = serializeJSONMerchantParams(requestParams);
3392
- return sha256SignJSONRequest(merchantKey, serializedParams, requestParams);
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 !== expSignature) {
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: base64url.encode(JSON.stringify(methodData))
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(base64url.decode(threeDSMethodData));
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(/&(lt|#60|gt|#62|quot|#34|amp|#38|apos|#39);/g, (_match, p1) => {
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;
@@ -1,3 +1,3 @@
1
- import type { ResponseJSONSuccess, CommonRawRequestParams, CommonRawResponseParams, SHA256SignedJSONParameters } from '../types/api';
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) => SHA256SignedJSONParameters;
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;
@@ -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>;
@@ -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: SHA256SignedJSONParameters;
105
+ body: SignedJSONParameters;
80
106
  }
@@ -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": "5.3.2",
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": "^7.0.0"
47
+ "undici": "^8.0.1",
48
+ "vitest": "^5.1.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@commitlint/cli": "^17.6.7",
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.28.0",
55
- "@microsoft/api-extractor": "^7.36.3",
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.5.0",
64
- "eslint": "^9.39.1",
65
- "eslint-config-love": "^114.0.0",
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.2",
69
+ "rimraf": "^6.1.3",
73
70
  "rollup": "^3.29.5",
74
- "rollup-plugin-typescript2": "^0.35.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": "jest ./src/*.test.ts ./src/**/*.test.ts",
110
- "test:integration": "jest ./test/integration/**/*.test.ts",
111
- "test:code": "jest",
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
  }