@paykit-sdk/redsys 1.2.0 → 1.3.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/dist/index.js CHANGED
@@ -22,6 +22,12 @@ var CURRENCY_MAP = {
22
22
  GBP: 826,
23
23
  JPY: 392
24
24
  };
25
+ var CURRENCY_NUM_TO_CODE = Object.fromEntries(
26
+ Object.entries(CURRENCY_MAP).map(([code, num]) => [
27
+ String(num),
28
+ code
29
+ ])
30
+ );
25
31
  var RESPONSE_CODES = {
26
32
  "0000": "Transaction approved",
27
33
  "0101": "Card expired",
@@ -57,7 +63,7 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
57
63
  }
58
64
  _getRedsysUrl() {
59
65
  if (this.opts.redsysUrl) return this.opts.redsysUrl;
60
- return this.opts.isSandbox ? "https://sis.redsys.es/sis/realizarPago" : "https://sis-t.REDsys.es:25443/sis/realizarPago";
66
+ return this.opts.isSandbox ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago";
61
67
  }
62
68
  _getCurrencyNum(currency) {
63
69
  return CURRENCY_MAP[currency.toUpperCase()] ?? 978;
@@ -85,17 +91,35 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
85
91
  return Buffer.from(jsonStr).toString("base64");
86
92
  }
87
93
  /**
88
- * Generate HMAC-SHA256 signature for inSite
89
- *
90
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
91
- * Order is important: base64Params + orderId (both as-is, no separators)
94
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
95
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
96
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
97
+ */
98
+ _deriveOrderKey(orderId) {
99
+ const key = Buffer.from(this.opts.secretKey, "base64");
100
+ const iv = Buffer.alloc(8, 0);
101
+ const cipher = crypto.createCipheriv("des-ede3-cbc", key, iv);
102
+ cipher.setAutoPadding(false);
103
+ const padded = Buffer.alloc(Math.ceil(orderId.length / 8) * 8, 0);
104
+ padded.write(orderId, "utf8");
105
+ return Buffer.concat([cipher.update(padded), cipher.final()]);
106
+ }
107
+ /**
108
+ * Redsys HMAC_SHA256_V1 signature:
109
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
92
110
  */
93
111
  _sign(orderId, base64Params) {
94
- const data = base64Params + orderId;
95
- return crypto.createHmac(
96
- "sha256",
97
- Buffer.from(this.opts.secretKey, "base64")
98
- ).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "/").replace(/=+$/, "");
112
+ return crypto.createHmac("sha256", this._deriveOrderKey(orderId)).update(base64Params).digest("base64");
113
+ }
114
+ /**
115
+ * Compare signatures in constant time, accepting both standard and
116
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
117
+ */
118
+ _signatureMatches(received, expected) {
119
+ const normalize = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
120
+ const receivedBuf = normalize(received);
121
+ const expectedBuf = normalize(expected);
122
+ return receivedBuf.length === expectedBuf.length && crypto.timingSafeEqual(receivedBuf, expectedBuf);
99
123
  }
100
124
  /**
101
125
  * Create checkout for inSite
@@ -299,9 +323,13 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
299
323
  );
300
324
  };
301
325
  createCustomer = async (params) => {
302
- throw new core.ProviderNotSupportedError("createCustomer", "gopay", {
303
- reason: "Redsys doesn't support creating customers"
304
- });
326
+ throw new core.ProviderNotSupportedError(
327
+ "createCustomer",
328
+ this.providerName,
329
+ {
330
+ reason: "Redsys doesn't support creating customers"
331
+ }
332
+ );
305
333
  };
306
334
  retrieveCustomer = async (_id) => {
307
335
  return null;
@@ -435,33 +463,38 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
435
463
  const dsOrder = params.Ds_Order;
436
464
  const dsAmount = params.Ds_Amount;
437
465
  const dsMerchantData = params.Ds_MerchantData;
466
+ const currency = CURRENCY_NUM_TO_CODE[String(params.Ds_Currency ?? "")] ?? "EUR";
438
467
  const signature = dataAsObject.Ds_Signature;
439
468
  const expectedSig = this._sign(dsOrder, paramsBase64);
440
- const sigOk = signature === expectedSig;
441
- if (!sigOk) {
469
+ if (!signature || !this._signatureMatches(signature, expectedSig)) {
442
470
  throw new core.WebhookError("Invalid Redsys webhook signature", {
443
471
  provider: this.providerName
444
472
  });
445
473
  }
446
474
  const events = [];
475
+ const amountNum = Number(dsAmount);
447
476
  if (dsResponse === "0000" || dsResponse.startsWith("00")) {
448
- const customerId = core.parseJSON(
449
- Buffer.from(dsMerchantData, "base64").toString("utf-8"),
477
+ const customerId = dsMerchantData ? core.parseJSON(
478
+ Buffer.from(String(dsMerchantData), "base64").toString(
479
+ "utf-8"
480
+ ),
450
481
  core.Schema.object({
451
482
  customerId: core.Schema.string().optional()
452
483
  })
453
- )?.customerId ?? null;
454
- const amountNum = Number(dsAmount) / 100;
484
+ )?.customerId ?? null : null;
455
485
  const paymentId = `${dsOrder}_${params.Ds_AuthorisationCode ?? "unknown"}`;
456
486
  events.push(
457
487
  {
458
- event: "redsys.payment.succeeded",
488
+ id: crypto.randomBytes(8).toString("hex"),
489
+ type: "redsys.payment.succeeded",
490
+ created: Math.floor(Date.now() / 1e3),
459
491
  data: {
460
492
  order_id: dsOrder,
461
493
  amount: amountNum,
462
494
  response_code: dsResponse,
463
495
  customer_id: customerId
464
- }
496
+ },
497
+ is_raw: true
465
498
  },
466
499
  core.paykitEvent$InboundSchema({
467
500
  type: "payment.succeeded",
@@ -470,7 +503,7 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
470
503
  data: {
471
504
  id: paymentId,
472
505
  amount: amountNum,
473
- currency: "EUR",
506
+ currency,
474
507
  customer: customerId ? { id: customerId } : null,
475
508
  status: "succeeded",
476
509
  metadata: {},
@@ -483,12 +516,15 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
483
516
  } else {
484
517
  events.push(
485
518
  {
486
- event: "redsys.payment.failed",
519
+ id: crypto.randomBytes(8).toString("hex"),
520
+ type: "redsys.payment.failed",
521
+ created: Math.floor(Date.now() / 1e3),
487
522
  data: {
488
523
  order_id: dsOrder,
489
524
  response_code: dsResponse,
490
525
  error_message: RESPONSE_CODES[dsResponse] ?? `Code ${dsResponse}`
491
- }
526
+ },
527
+ is_raw: true
492
528
  },
493
529
  core.paykitEvent$InboundSchema({
494
530
  type: "payment.failed",
@@ -496,8 +532,8 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
496
532
  id: crypto.randomBytes(8).toString("hex").slice(0, 15),
497
533
  data: {
498
534
  id: dsOrder,
499
- amount: Number(dsAmount) / 100,
500
- currency: "EUR",
535
+ amount: amountNum,
536
+ currency,
501
537
  customer: null,
502
538
  status: "failed",
503
539
  metadata: {},
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { schema, Schema, AbstractPayKitProvider, HTTPClient, NotImplementedError, createCheckoutSchema, ValidationError, validateRequiredKeys, isIdCustomer, isEmailCustomer, stringifyMetadataValues, PAYKIT_METADATA_KEY, ResourceNotFoundError, createPaymentSchema, OperationFailedError, ProviderNotSupportedError, createRefundSchema, WebhookError, parseJSON, paykitEvent$InboundSchema } from '@paykit-sdk/core';
2
- import { randomBytes, createHmac } from 'crypto';
2
+ import { randomBytes, createCipheriv, createHmac, timingSafeEqual } from 'crypto';
3
3
 
4
4
  // src/index.ts
5
5
  var RedsysOptionsSchema = schema()(
@@ -20,6 +20,12 @@ var CURRENCY_MAP = {
20
20
  GBP: 826,
21
21
  JPY: 392
22
22
  };
23
+ var CURRENCY_NUM_TO_CODE = Object.fromEntries(
24
+ Object.entries(CURRENCY_MAP).map(([code, num]) => [
25
+ String(num),
26
+ code
27
+ ])
28
+ );
23
29
  var RESPONSE_CODES = {
24
30
  "0000": "Transaction approved",
25
31
  "0101": "Card expired",
@@ -55,7 +61,7 @@ var RedsysProvider = class extends AbstractPayKitProvider {
55
61
  }
56
62
  _getRedsysUrl() {
57
63
  if (this.opts.redsysUrl) return this.opts.redsysUrl;
58
- return this.opts.isSandbox ? "https://sis.redsys.es/sis/realizarPago" : "https://sis-t.REDsys.es:25443/sis/realizarPago";
64
+ return this.opts.isSandbox ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago";
59
65
  }
60
66
  _getCurrencyNum(currency) {
61
67
  return CURRENCY_MAP[currency.toUpperCase()] ?? 978;
@@ -83,17 +89,35 @@ var RedsysProvider = class extends AbstractPayKitProvider {
83
89
  return Buffer.from(jsonStr).toString("base64");
84
90
  }
85
91
  /**
86
- * Generate HMAC-SHA256 signature for inSite
87
- *
88
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
89
- * Order is important: base64Params + orderId (both as-is, no separators)
92
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
93
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
94
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
95
+ */
96
+ _deriveOrderKey(orderId) {
97
+ const key = Buffer.from(this.opts.secretKey, "base64");
98
+ const iv = Buffer.alloc(8, 0);
99
+ const cipher = createCipheriv("des-ede3-cbc", key, iv);
100
+ cipher.setAutoPadding(false);
101
+ const padded = Buffer.alloc(Math.ceil(orderId.length / 8) * 8, 0);
102
+ padded.write(orderId, "utf8");
103
+ return Buffer.concat([cipher.update(padded), cipher.final()]);
104
+ }
105
+ /**
106
+ * Redsys HMAC_SHA256_V1 signature:
107
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
90
108
  */
91
109
  _sign(orderId, base64Params) {
92
- const data = base64Params + orderId;
93
- return createHmac(
94
- "sha256",
95
- Buffer.from(this.opts.secretKey, "base64")
96
- ).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "/").replace(/=+$/, "");
110
+ return createHmac("sha256", this._deriveOrderKey(orderId)).update(base64Params).digest("base64");
111
+ }
112
+ /**
113
+ * Compare signatures in constant time, accepting both standard and
114
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
115
+ */
116
+ _signatureMatches(received, expected) {
117
+ const normalize = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
118
+ const receivedBuf = normalize(received);
119
+ const expectedBuf = normalize(expected);
120
+ return receivedBuf.length === expectedBuf.length && timingSafeEqual(receivedBuf, expectedBuf);
97
121
  }
98
122
  /**
99
123
  * Create checkout for inSite
@@ -297,9 +321,13 @@ var RedsysProvider = class extends AbstractPayKitProvider {
297
321
  );
298
322
  };
299
323
  createCustomer = async (params) => {
300
- throw new ProviderNotSupportedError("createCustomer", "gopay", {
301
- reason: "Redsys doesn't support creating customers"
302
- });
324
+ throw new ProviderNotSupportedError(
325
+ "createCustomer",
326
+ this.providerName,
327
+ {
328
+ reason: "Redsys doesn't support creating customers"
329
+ }
330
+ );
303
331
  };
304
332
  retrieveCustomer = async (_id) => {
305
333
  return null;
@@ -433,33 +461,38 @@ var RedsysProvider = class extends AbstractPayKitProvider {
433
461
  const dsOrder = params.Ds_Order;
434
462
  const dsAmount = params.Ds_Amount;
435
463
  const dsMerchantData = params.Ds_MerchantData;
464
+ const currency = CURRENCY_NUM_TO_CODE[String(params.Ds_Currency ?? "")] ?? "EUR";
436
465
  const signature = dataAsObject.Ds_Signature;
437
466
  const expectedSig = this._sign(dsOrder, paramsBase64);
438
- const sigOk = signature === expectedSig;
439
- if (!sigOk) {
467
+ if (!signature || !this._signatureMatches(signature, expectedSig)) {
440
468
  throw new WebhookError("Invalid Redsys webhook signature", {
441
469
  provider: this.providerName
442
470
  });
443
471
  }
444
472
  const events = [];
473
+ const amountNum = Number(dsAmount);
445
474
  if (dsResponse === "0000" || dsResponse.startsWith("00")) {
446
- const customerId = parseJSON(
447
- Buffer.from(dsMerchantData, "base64").toString("utf-8"),
475
+ const customerId = dsMerchantData ? parseJSON(
476
+ Buffer.from(String(dsMerchantData), "base64").toString(
477
+ "utf-8"
478
+ ),
448
479
  Schema.object({
449
480
  customerId: Schema.string().optional()
450
481
  })
451
- )?.customerId ?? null;
452
- const amountNum = Number(dsAmount) / 100;
482
+ )?.customerId ?? null : null;
453
483
  const paymentId = `${dsOrder}_${params.Ds_AuthorisationCode ?? "unknown"}`;
454
484
  events.push(
455
485
  {
456
- event: "redsys.payment.succeeded",
486
+ id: randomBytes(8).toString("hex"),
487
+ type: "redsys.payment.succeeded",
488
+ created: Math.floor(Date.now() / 1e3),
457
489
  data: {
458
490
  order_id: dsOrder,
459
491
  amount: amountNum,
460
492
  response_code: dsResponse,
461
493
  customer_id: customerId
462
- }
494
+ },
495
+ is_raw: true
463
496
  },
464
497
  paykitEvent$InboundSchema({
465
498
  type: "payment.succeeded",
@@ -468,7 +501,7 @@ var RedsysProvider = class extends AbstractPayKitProvider {
468
501
  data: {
469
502
  id: paymentId,
470
503
  amount: amountNum,
471
- currency: "EUR",
504
+ currency,
472
505
  customer: customerId ? { id: customerId } : null,
473
506
  status: "succeeded",
474
507
  metadata: {},
@@ -481,12 +514,15 @@ var RedsysProvider = class extends AbstractPayKitProvider {
481
514
  } else {
482
515
  events.push(
483
516
  {
484
- event: "redsys.payment.failed",
517
+ id: randomBytes(8).toString("hex"),
518
+ type: "redsys.payment.failed",
519
+ created: Math.floor(Date.now() / 1e3),
485
520
  data: {
486
521
  order_id: dsOrder,
487
522
  response_code: dsResponse,
488
523
  error_message: RESPONSE_CODES[dsResponse] ?? `Code ${dsResponse}`
489
- }
524
+ },
525
+ is_raw: true
490
526
  },
491
527
  paykitEvent$InboundSchema({
492
528
  type: "payment.failed",
@@ -494,8 +530,8 @@ var RedsysProvider = class extends AbstractPayKitProvider {
494
530
  id: randomBytes(8).toString("hex").slice(0, 15),
495
531
  data: {
496
532
  id: dsOrder,
497
- amount: Number(dsAmount) / 100,
498
- currency: "EUR",
533
+ amount: amountNum,
534
+ currency,
499
535
  customer: null,
500
536
  status: "failed",
501
537
  metadata: {},
@@ -64,12 +64,21 @@ declare class RedsysProvider extends AbstractPayKitProvider implements PayKitPro
64
64
  */
65
65
  private _buildMerchantParams;
66
66
  /**
67
- * Generate HMAC-SHA256 signature for inSite
68
- *
69
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
70
- * Order is important: base64Params + orderId (both as-is, no separators)
67
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
68
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
69
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
70
+ */
71
+ private _deriveOrderKey;
72
+ /**
73
+ * Redsys HMAC_SHA256_V1 signature:
74
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
71
75
  */
72
76
  private _sign;
77
+ /**
78
+ * Compare signatures in constant time, accepting both standard and
79
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
80
+ */
81
+ private _signatureMatches;
73
82
  /**
74
83
  * Create checkout for inSite
75
84
  *
@@ -64,12 +64,21 @@ declare class RedsysProvider extends AbstractPayKitProvider implements PayKitPro
64
64
  */
65
65
  private _buildMerchantParams;
66
66
  /**
67
- * Generate HMAC-SHA256 signature for inSite
68
- *
69
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
70
- * Order is important: base64Params + orderId (both as-is, no separators)
67
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
68
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
69
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
70
+ */
71
+ private _deriveOrderKey;
72
+ /**
73
+ * Redsys HMAC_SHA256_V1 signature:
74
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
71
75
  */
72
76
  private _sign;
77
+ /**
78
+ * Compare signatures in constant time, accepting both standard and
79
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
80
+ */
81
+ private _signatureMatches;
73
82
  /**
74
83
  * Create checkout for inSite
75
84
  *
@@ -22,6 +22,12 @@ var CURRENCY_MAP = {
22
22
  GBP: 826,
23
23
  JPY: 392
24
24
  };
25
+ var CURRENCY_NUM_TO_CODE = Object.fromEntries(
26
+ Object.entries(CURRENCY_MAP).map(([code, num]) => [
27
+ String(num),
28
+ code
29
+ ])
30
+ );
25
31
  var RESPONSE_CODES = {
26
32
  "0000": "Transaction approved",
27
33
  "0101": "Card expired",
@@ -57,7 +63,7 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
57
63
  }
58
64
  _getRedsysUrl() {
59
65
  if (this.opts.redsysUrl) return this.opts.redsysUrl;
60
- return this.opts.isSandbox ? "https://sis.redsys.es/sis/realizarPago" : "https://sis-t.REDsys.es:25443/sis/realizarPago";
66
+ return this.opts.isSandbox ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago";
61
67
  }
62
68
  _getCurrencyNum(currency) {
63
69
  return CURRENCY_MAP[currency.toUpperCase()] ?? 978;
@@ -85,17 +91,35 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
85
91
  return Buffer.from(jsonStr).toString("base64");
86
92
  }
87
93
  /**
88
- * Generate HMAC-SHA256 signature for inSite
89
- *
90
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
91
- * Order is important: base64Params + orderId (both as-is, no separators)
94
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
95
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
96
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
97
+ */
98
+ _deriveOrderKey(orderId) {
99
+ const key = Buffer.from(this.opts.secretKey, "base64");
100
+ const iv = Buffer.alloc(8, 0);
101
+ const cipher = crypto.createCipheriv("des-ede3-cbc", key, iv);
102
+ cipher.setAutoPadding(false);
103
+ const padded = Buffer.alloc(Math.ceil(orderId.length / 8) * 8, 0);
104
+ padded.write(orderId, "utf8");
105
+ return Buffer.concat([cipher.update(padded), cipher.final()]);
106
+ }
107
+ /**
108
+ * Redsys HMAC_SHA256_V1 signature:
109
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
92
110
  */
93
111
  _sign(orderId, base64Params) {
94
- const data = base64Params + orderId;
95
- return crypto.createHmac(
96
- "sha256",
97
- Buffer.from(this.opts.secretKey, "base64")
98
- ).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "/").replace(/=+$/, "");
112
+ return crypto.createHmac("sha256", this._deriveOrderKey(orderId)).update(base64Params).digest("base64");
113
+ }
114
+ /**
115
+ * Compare signatures in constant time, accepting both standard and
116
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
117
+ */
118
+ _signatureMatches(received, expected) {
119
+ const normalize = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
120
+ const receivedBuf = normalize(received);
121
+ const expectedBuf = normalize(expected);
122
+ return receivedBuf.length === expectedBuf.length && crypto.timingSafeEqual(receivedBuf, expectedBuf);
99
123
  }
100
124
  /**
101
125
  * Create checkout for inSite
@@ -299,9 +323,13 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
299
323
  );
300
324
  };
301
325
  createCustomer = async (params) => {
302
- throw new core.ProviderNotSupportedError("createCustomer", "gopay", {
303
- reason: "Redsys doesn't support creating customers"
304
- });
326
+ throw new core.ProviderNotSupportedError(
327
+ "createCustomer",
328
+ this.providerName,
329
+ {
330
+ reason: "Redsys doesn't support creating customers"
331
+ }
332
+ );
305
333
  };
306
334
  retrieveCustomer = async (_id) => {
307
335
  return null;
@@ -435,33 +463,38 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
435
463
  const dsOrder = params.Ds_Order;
436
464
  const dsAmount = params.Ds_Amount;
437
465
  const dsMerchantData = params.Ds_MerchantData;
466
+ const currency = CURRENCY_NUM_TO_CODE[String(params.Ds_Currency ?? "")] ?? "EUR";
438
467
  const signature = dataAsObject.Ds_Signature;
439
468
  const expectedSig = this._sign(dsOrder, paramsBase64);
440
- const sigOk = signature === expectedSig;
441
- if (!sigOk) {
469
+ if (!signature || !this._signatureMatches(signature, expectedSig)) {
442
470
  throw new core.WebhookError("Invalid Redsys webhook signature", {
443
471
  provider: this.providerName
444
472
  });
445
473
  }
446
474
  const events = [];
475
+ const amountNum = Number(dsAmount);
447
476
  if (dsResponse === "0000" || dsResponse.startsWith("00")) {
448
- const customerId = core.parseJSON(
449
- Buffer.from(dsMerchantData, "base64").toString("utf-8"),
477
+ const customerId = dsMerchantData ? core.parseJSON(
478
+ Buffer.from(String(dsMerchantData), "base64").toString(
479
+ "utf-8"
480
+ ),
450
481
  core.Schema.object({
451
482
  customerId: core.Schema.string().optional()
452
483
  })
453
- )?.customerId ?? null;
454
- const amountNum = Number(dsAmount) / 100;
484
+ )?.customerId ?? null : null;
455
485
  const paymentId = `${dsOrder}_${params.Ds_AuthorisationCode ?? "unknown"}`;
456
486
  events.push(
457
487
  {
458
- event: "redsys.payment.succeeded",
488
+ id: crypto.randomBytes(8).toString("hex"),
489
+ type: "redsys.payment.succeeded",
490
+ created: Math.floor(Date.now() / 1e3),
459
491
  data: {
460
492
  order_id: dsOrder,
461
493
  amount: amountNum,
462
494
  response_code: dsResponse,
463
495
  customer_id: customerId
464
- }
496
+ },
497
+ is_raw: true
465
498
  },
466
499
  core.paykitEvent$InboundSchema({
467
500
  type: "payment.succeeded",
@@ -470,7 +503,7 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
470
503
  data: {
471
504
  id: paymentId,
472
505
  amount: amountNum,
473
- currency: "EUR",
506
+ currency,
474
507
  customer: customerId ? { id: customerId } : null,
475
508
  status: "succeeded",
476
509
  metadata: {},
@@ -483,12 +516,15 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
483
516
  } else {
484
517
  events.push(
485
518
  {
486
- event: "redsys.payment.failed",
519
+ id: crypto.randomBytes(8).toString("hex"),
520
+ type: "redsys.payment.failed",
521
+ created: Math.floor(Date.now() / 1e3),
487
522
  data: {
488
523
  order_id: dsOrder,
489
524
  response_code: dsResponse,
490
525
  error_message: RESPONSE_CODES[dsResponse] ?? `Code ${dsResponse}`
491
- }
526
+ },
527
+ is_raw: true
492
528
  },
493
529
  core.paykitEvent$InboundSchema({
494
530
  type: "payment.failed",
@@ -496,8 +532,8 @@ var RedsysProvider = class extends core.AbstractPayKitProvider {
496
532
  id: crypto.randomBytes(8).toString("hex").slice(0, 15),
497
533
  data: {
498
534
  id: dsOrder,
499
- amount: Number(dsAmount) / 100,
500
- currency: "EUR",
535
+ amount: amountNum,
536
+ currency,
501
537
  customer: null,
502
538
  status: "failed",
503
539
  metadata: {},
@@ -1,5 +1,5 @@
1
1
  import { schema, Schema, AbstractPayKitProvider, HTTPClient, NotImplementedError, createCheckoutSchema, ValidationError, validateRequiredKeys, isIdCustomer, isEmailCustomer, stringifyMetadataValues, PAYKIT_METADATA_KEY, ResourceNotFoundError, createPaymentSchema, OperationFailedError, ProviderNotSupportedError, createRefundSchema, WebhookError, parseJSON, paykitEvent$InboundSchema } from '@paykit-sdk/core';
2
- import { randomBytes, createHmac } from 'crypto';
2
+ import { randomBytes, createCipheriv, createHmac, timingSafeEqual } from 'crypto';
3
3
 
4
4
  // src/redsys-provider.ts
5
5
  var RedsysOptionsSchema = schema()(
@@ -20,6 +20,12 @@ var CURRENCY_MAP = {
20
20
  GBP: 826,
21
21
  JPY: 392
22
22
  };
23
+ var CURRENCY_NUM_TO_CODE = Object.fromEntries(
24
+ Object.entries(CURRENCY_MAP).map(([code, num]) => [
25
+ String(num),
26
+ code
27
+ ])
28
+ );
23
29
  var RESPONSE_CODES = {
24
30
  "0000": "Transaction approved",
25
31
  "0101": "Card expired",
@@ -55,7 +61,7 @@ var RedsysProvider = class extends AbstractPayKitProvider {
55
61
  }
56
62
  _getRedsysUrl() {
57
63
  if (this.opts.redsysUrl) return this.opts.redsysUrl;
58
- return this.opts.isSandbox ? "https://sis.redsys.es/sis/realizarPago" : "https://sis-t.REDsys.es:25443/sis/realizarPago";
64
+ return this.opts.isSandbox ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago";
59
65
  }
60
66
  _getCurrencyNum(currency) {
61
67
  return CURRENCY_MAP[currency.toUpperCase()] ?? 978;
@@ -83,17 +89,35 @@ var RedsysProvider = class extends AbstractPayKitProvider {
83
89
  return Buffer.from(jsonStr).toString("base64");
84
90
  }
85
91
  /**
86
- * Generate HMAC-SHA256 signature for inSite
87
- *
88
- * Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
89
- * Order is important: base64Params + orderId (both as-is, no separators)
92
+ * Derive the per-order key required by Redsys HMAC_SHA256_V1:
93
+ * 3DES-CBC-encrypt the order number (zero-padded to a multiple of
94
+ * 8 bytes) with the base64-decoded merchant secret and a zero IV.
95
+ */
96
+ _deriveOrderKey(orderId) {
97
+ const key = Buffer.from(this.opts.secretKey, "base64");
98
+ const iv = Buffer.alloc(8, 0);
99
+ const cipher = createCipheriv("des-ede3-cbc", key, iv);
100
+ cipher.setAutoPadding(false);
101
+ const padded = Buffer.alloc(Math.ceil(orderId.length / 8) * 8, 0);
102
+ padded.write(orderId, "utf8");
103
+ return Buffer.concat([cipher.update(padded), cipher.final()]);
104
+ }
105
+ /**
106
+ * Redsys HMAC_SHA256_V1 signature:
107
+ * HMAC-SHA256(deriveOrderKey(order), base64Params) as Base64.
90
108
  */
91
109
  _sign(orderId, base64Params) {
92
- const data = base64Params + orderId;
93
- return createHmac(
94
- "sha256",
95
- Buffer.from(this.opts.secretKey, "base64")
96
- ).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "/").replace(/=+$/, "");
110
+ return createHmac("sha256", this._deriveOrderKey(orderId)).update(base64Params).digest("base64");
111
+ }
112
+ /**
113
+ * Compare signatures in constant time, accepting both standard and
114
+ * URL-safe Base64 (Redsys uses URL-safe in some channels).
115
+ */
116
+ _signatureMatches(received, expected) {
117
+ const normalize = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
118
+ const receivedBuf = normalize(received);
119
+ const expectedBuf = normalize(expected);
120
+ return receivedBuf.length === expectedBuf.length && timingSafeEqual(receivedBuf, expectedBuf);
97
121
  }
98
122
  /**
99
123
  * Create checkout for inSite
@@ -297,9 +321,13 @@ var RedsysProvider = class extends AbstractPayKitProvider {
297
321
  );
298
322
  };
299
323
  createCustomer = async (params) => {
300
- throw new ProviderNotSupportedError("createCustomer", "gopay", {
301
- reason: "Redsys doesn't support creating customers"
302
- });
324
+ throw new ProviderNotSupportedError(
325
+ "createCustomer",
326
+ this.providerName,
327
+ {
328
+ reason: "Redsys doesn't support creating customers"
329
+ }
330
+ );
303
331
  };
304
332
  retrieveCustomer = async (_id) => {
305
333
  return null;
@@ -433,33 +461,38 @@ var RedsysProvider = class extends AbstractPayKitProvider {
433
461
  const dsOrder = params.Ds_Order;
434
462
  const dsAmount = params.Ds_Amount;
435
463
  const dsMerchantData = params.Ds_MerchantData;
464
+ const currency = CURRENCY_NUM_TO_CODE[String(params.Ds_Currency ?? "")] ?? "EUR";
436
465
  const signature = dataAsObject.Ds_Signature;
437
466
  const expectedSig = this._sign(dsOrder, paramsBase64);
438
- const sigOk = signature === expectedSig;
439
- if (!sigOk) {
467
+ if (!signature || !this._signatureMatches(signature, expectedSig)) {
440
468
  throw new WebhookError("Invalid Redsys webhook signature", {
441
469
  provider: this.providerName
442
470
  });
443
471
  }
444
472
  const events = [];
473
+ const amountNum = Number(dsAmount);
445
474
  if (dsResponse === "0000" || dsResponse.startsWith("00")) {
446
- const customerId = parseJSON(
447
- Buffer.from(dsMerchantData, "base64").toString("utf-8"),
475
+ const customerId = dsMerchantData ? parseJSON(
476
+ Buffer.from(String(dsMerchantData), "base64").toString(
477
+ "utf-8"
478
+ ),
448
479
  Schema.object({
449
480
  customerId: Schema.string().optional()
450
481
  })
451
- )?.customerId ?? null;
452
- const amountNum = Number(dsAmount) / 100;
482
+ )?.customerId ?? null : null;
453
483
  const paymentId = `${dsOrder}_${params.Ds_AuthorisationCode ?? "unknown"}`;
454
484
  events.push(
455
485
  {
456
- event: "redsys.payment.succeeded",
486
+ id: randomBytes(8).toString("hex"),
487
+ type: "redsys.payment.succeeded",
488
+ created: Math.floor(Date.now() / 1e3),
457
489
  data: {
458
490
  order_id: dsOrder,
459
491
  amount: amountNum,
460
492
  response_code: dsResponse,
461
493
  customer_id: customerId
462
- }
494
+ },
495
+ is_raw: true
463
496
  },
464
497
  paykitEvent$InboundSchema({
465
498
  type: "payment.succeeded",
@@ -468,7 +501,7 @@ var RedsysProvider = class extends AbstractPayKitProvider {
468
501
  data: {
469
502
  id: paymentId,
470
503
  amount: amountNum,
471
- currency: "EUR",
504
+ currency,
472
505
  customer: customerId ? { id: customerId } : null,
473
506
  status: "succeeded",
474
507
  metadata: {},
@@ -481,12 +514,15 @@ var RedsysProvider = class extends AbstractPayKitProvider {
481
514
  } else {
482
515
  events.push(
483
516
  {
484
- event: "redsys.payment.failed",
517
+ id: randomBytes(8).toString("hex"),
518
+ type: "redsys.payment.failed",
519
+ created: Math.floor(Date.now() / 1e3),
485
520
  data: {
486
521
  order_id: dsOrder,
487
522
  response_code: dsResponse,
488
523
  error_message: RESPONSE_CODES[dsResponse] ?? `Code ${dsResponse}`
489
- }
524
+ },
525
+ is_raw: true
490
526
  },
491
527
  paykitEvent$InboundSchema({
492
528
  type: "payment.failed",
@@ -494,8 +530,8 @@ var RedsysProvider = class extends AbstractPayKitProvider {
494
530
  id: randomBytes(8).toString("hex").slice(0, 15),
495
531
  data: {
496
532
  id: dsOrder,
497
- amount: Number(dsAmount) / 100,
498
- currency: "EUR",
533
+ amount: amountNum,
534
+ currency,
499
535
  customer: null,
500
536
  status: "failed",
501
537
  metadata: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paykit-sdk/redsys",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Redsys inSite provider for PayKit",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -8,9 +8,6 @@
8
8
  "files": [
9
9
  "dist"
10
10
  ],
11
- "scripts": {
12
- "build": "tsup"
13
- },
14
11
  "paykit": {
15
12
  "type": "provider"
16
13
  },
@@ -27,12 +24,12 @@
27
24
  ],
28
25
  "license": "MIT",
29
26
  "peerDependencies": {
30
- "@paykit-sdk/core": ">=1.2.0"
27
+ "@paykit-sdk/core": "^1.2.3"
31
28
  },
32
29
  "devDependencies": {
33
- "@paykit-sdk/core": "workspace:*",
34
30
  "tsup": "^8.5.0",
35
- "typescript": "^5.9.2"
31
+ "typescript": "^5.9.2",
32
+ "@paykit-sdk/core": "1.3.0"
36
33
  },
37
34
  "publishConfig": {
38
35
  "access": "public"
@@ -43,5 +40,10 @@
43
40
  },
44
41
  "bugs": {
45
42
  "url": "https://github.com/usepaykit/paykit-sdk/issues"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "test": "vitest run --config ../../vitest.config.ts packages/redsys/src",
47
+ "typecheck": "tsc --noEmit"
46
48
  }
47
- }
49
+ }