@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 +62 -26
- package/dist/index.mjs +63 -27
- package/dist/redsys-provider.d.mts +13 -4
- package/dist/redsys-provider.d.ts +13 -4
- package/dist/redsys-provider.js +62 -26
- package/dist/redsys-provider.mjs +63 -27
- package/package.json +10 -8
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
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
303
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
500
|
-
currency
|
|
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
|
|
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
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
301
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
498
|
-
currency
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
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
|
*
|
package/dist/redsys-provider.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
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
303
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
500
|
-
currency
|
|
535
|
+
amount: amountNum,
|
|
536
|
+
currency,
|
|
501
537
|
customer: null,
|
|
502
538
|
status: "failed",
|
|
503
539
|
metadata: {},
|
package/dist/redsys-provider.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/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
|
|
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
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
301
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
498
|
-
currency
|
|
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.
|
|
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": "
|
|
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
|
+
}
|