simplepay-js-sdk 0.11.1 → 0.12.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 +29 -3
- package/dist/index.d.ts +38 -5
- package/dist/index.js +76 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -3
- package/src/utils.spec.ts +181 -1
- package/src/utils.ts +71 -1
package/README.md
CHANGED
|
@@ -95,12 +95,38 @@ A `response` a következő tulajdonságokkal rendelkezik:
|
|
|
95
95
|
#### IPN végpont
|
|
96
96
|
|
|
97
97
|
A SimplePay `POST` kérést küld az IPN URL-re, és válaszolnunk kell rá.
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
**Ajánlott megközelítés a `handleIpnRequest` függvénnyel:**
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { handleIpnRequest } from 'simplepay-js-sdk'
|
|
103
|
+
|
|
104
|
+
// Az IPN végpont kezelőjében (pl. Express, Next.js, stb.)
|
|
105
|
+
const ipnBody = await request.text() // A nyers body stringként (fontos: használd a .text()-et, ne a JSON.parse()-t)
|
|
106
|
+
const incomingSignature = request.headers.get('Signature')
|
|
107
|
+
const { MERCHANT_KEY } = getSimplePayConfig('HUF') // vagy a te valutád
|
|
108
|
+
|
|
109
|
+
const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
|
|
110
|
+
|
|
111
|
+
// Válasz küldése HTTP 200 státusszal
|
|
112
|
+
return new Response(responseBody, {
|
|
113
|
+
status: 200,
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
'Signature': signature
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Manuális megközelítés:**
|
|
122
|
+
|
|
123
|
+
Ha manuálisan szeretnéd kezelni:
|
|
99
124
|
|
|
100
125
|
- ellenőrizd az aláírás érvényességét - használd a `checkSignature(ipnBody, signatureHeader, SIMPLEPAY_MERCHANT_KEY_HUF)` függvényt
|
|
101
|
-
- adj hozzá egy `receiveDate` tulajdonságot a kapott JSON-hoz
|
|
126
|
+
- adj hozzá egy `receiveDate` tulajdonságot a kapott JSON-hoz (ISO 8601 formátumban, pl. `2025-10-06T07:00:34+02:00`)
|
|
102
127
|
- számítsd ki az új aláírást - használd a `generateSignature(responseText, SIMPLEPAY_MERCHANT_KEY_HUF)` függvényt
|
|
103
|
-
- küldd el a `response`-t az új `signature`-rel
|
|
128
|
+
- küldd el a `response`-t az új `signature`-rel a HTTP fejlécben (ne a JSON body-ban)
|
|
129
|
+
- **Fontos**: A válasz JSON-nak tömörnek kell lennie (szóközmentes). A `JSON.stringify()` alapból tömör JSON-t ad vissza.
|
|
104
130
|
|
|
105
131
|
### Ismétlődő fizetés
|
|
106
132
|
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,39 @@ export declare const generateSignature: (body: string, merchantKey: string) => s
|
|
|
10
10
|
|
|
11
11
|
export declare const getPaymentResponse: (r: string, signature: string) => SimplePayResult;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Handles IPN (Instant Payment Notification) request and generates response
|
|
15
|
+
*
|
|
16
|
+
* This function implements the IPN flow according to SimplePay requirements:
|
|
17
|
+
* 1. Validates the incoming signature
|
|
18
|
+
* 2. Adds receiveDate property to the response
|
|
19
|
+
* 3. Generates response signature
|
|
20
|
+
*
|
|
21
|
+
* @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
|
|
22
|
+
* @param incomingSignature - The signature from the 'Signature' HTTP header
|
|
23
|
+
* @param merchantKey - The merchant secret key for signature validation and generation
|
|
24
|
+
* @returns Object containing the response JSON string and signature to send back
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // In your IPN endpoint handler:
|
|
28
|
+
* const ipnBody = await request.text() // Get raw body as string
|
|
29
|
+
* const incomingSignature = request.headers.get('Signature')
|
|
30
|
+
* const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
|
|
31
|
+
*
|
|
32
|
+
* // Send response with HTTP 200 status
|
|
33
|
+
* return new Response(responseBody, {
|
|
34
|
+
* status: 200,
|
|
35
|
+
* headers: {
|
|
36
|
+
* 'Content-Type': 'application/json',
|
|
37
|
+
* 'Signature': signature
|
|
38
|
+
* }
|
|
39
|
+
* })
|
|
40
|
+
*/
|
|
41
|
+
export declare const handleIpnRequest: (ipnBody: string, incomingSignature: string, merchantKey: string) => {
|
|
42
|
+
responseBody: string;
|
|
43
|
+
signature: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
13
46
|
declare type ISO8601DateString = string;
|
|
14
47
|
|
|
15
48
|
export declare type Language = typeof LANGUAGES[number];
|
|
@@ -52,7 +85,7 @@ declare interface RecurringPaymentData extends PaymentData {
|
|
|
52
85
|
recurring: Recurring;
|
|
53
86
|
}
|
|
54
87
|
|
|
55
|
-
declare interface SimplePayCancelCardResponse {
|
|
88
|
+
export declare interface SimplePayCancelCardResponse {
|
|
56
89
|
salt: string;
|
|
57
90
|
merchant: string;
|
|
58
91
|
cardId: string;
|
|
@@ -62,11 +95,11 @@ declare interface SimplePayCancelCardResponse {
|
|
|
62
95
|
|
|
63
96
|
declare type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL';
|
|
64
97
|
|
|
65
|
-
declare interface SimplePayRecurringResponse extends SimplePayResponse {
|
|
98
|
+
export declare interface SimplePayRecurringResponse extends SimplePayResponse {
|
|
66
99
|
tokens: string[];
|
|
67
100
|
}
|
|
68
101
|
|
|
69
|
-
declare interface SimplePayResponse {
|
|
102
|
+
export declare interface SimplePayResponse {
|
|
70
103
|
salt: string;
|
|
71
104
|
merchant: string;
|
|
72
105
|
orderRef: string;
|
|
@@ -78,7 +111,7 @@ declare interface SimplePayResponse {
|
|
|
78
111
|
errorCodes?: string[];
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
declare interface SimplePayResult {
|
|
114
|
+
export declare interface SimplePayResult {
|
|
82
115
|
responseCode: number;
|
|
83
116
|
transactionId: string;
|
|
84
117
|
event: SimplePayEvents;
|
|
@@ -87,7 +120,7 @@ declare interface SimplePayResult {
|
|
|
87
120
|
tokens?: string[];
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
declare interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> {
|
|
123
|
+
export declare interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> {
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
export declare const startPayment: (paymentData: PaymentData, config?: PaymentConfig) => Promise<SimplePayResponse>;
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import S from "crypto";
|
|
2
|
-
const
|
|
2
|
+
const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
|
|
3
3
|
process.env.SIMPLEPAY_LOGGER === "true" && console.log("👉 ", ...e);
|
|
4
4
|
}, E = (e) => {
|
|
5
|
-
if (!
|
|
5
|
+
if (!_.includes(e))
|
|
6
6
|
throw new Error(`Unsupported currency: ${e}`);
|
|
7
|
-
const
|
|
7
|
+
const t = "https://secure.simplepay.hu/payment/v2", r = "https://sandbox.simplepay.hu/payment/v2", n = "SimplePay_Rrd_0.11.2", s = process.env[`SIMPLEPAY_MERCHANT_KEY_${e}`], c = process.env[`SIMPLEPAY_MERCHANT_ID_${e}`], o = process.env.SIMPLEPAY_PRODUCTION === "true" ? t : r, u = o + "/start", a = o + "/dorecurring", R = o + "/cardcancel";
|
|
8
8
|
return {
|
|
9
9
|
MERCHANT_KEY: s,
|
|
10
10
|
MERCHANT_ID: c,
|
|
11
11
|
API_URL_PAYMENT: u,
|
|
12
|
-
API_URL_RECURRING:
|
|
13
|
-
API_URL_CARD_CANCEL:
|
|
14
|
-
SDK_VERSION:
|
|
12
|
+
API_URL_RECURRING: a,
|
|
13
|
+
API_URL_CARD_CANCEL: R,
|
|
14
|
+
SDK_VERSION: n
|
|
15
15
|
};
|
|
16
|
-
}, g = (e) => JSON.stringify(e).replace(/\//g, "\\/"), m = (e,
|
|
17
|
-
const r = S.createHmac("sha384",
|
|
16
|
+
}, g = (e) => JSON.stringify(e).replace(/\//g, "\\/"), m = (e, t) => {
|
|
17
|
+
const r = S.createHmac("sha384", t.trim());
|
|
18
18
|
return r.update(e, "utf8"), r.digest("base64");
|
|
19
|
-
},
|
|
20
|
-
const
|
|
21
|
-
([r,
|
|
19
|
+
}, d = (e, t, r) => t === m(e, r), l = (e) => e.toISOString().replace(/\.\d{3}Z$/, "+00:00"), I = (e) => {
|
|
20
|
+
const t = Object.entries(process.env).find(
|
|
21
|
+
([r, n]) => r.startsWith("SIMPLEPAY_MERCHANT_ID_") && n === e
|
|
22
22
|
)?.[0]?.replace("SIMPLEPAY_MERCHANT_ID_", "");
|
|
23
|
-
if (!
|
|
23
|
+
if (!t)
|
|
24
24
|
throw new Error(`Merchant id not found in the environment: ${e}`);
|
|
25
|
-
return
|
|
26
|
-
}, p = async (e,
|
|
27
|
-
const s = g(
|
|
28
|
-
i({ function: `SimplePay/makeRequest/${
|
|
25
|
+
return t;
|
|
26
|
+
}, p = async (e, t, r) => P(e, t, r, "oneTime"), f = async (e, t, r) => P(e, t, r, "recurring"), A = async (e, t, r) => P(e, t, r, "token"), h = async (e, t, r) => P(e, t, r, "cancelCard"), P = async (e, t, r, n) => {
|
|
27
|
+
const s = g(t), c = m(s, r);
|
|
28
|
+
i({ function: `SimplePay/makeRequest/${n}`, bodyString: s, signature: c });
|
|
29
29
|
try {
|
|
30
30
|
const o = await fetch(e, {
|
|
31
31
|
method: "POST",
|
|
@@ -35,25 +35,25 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
|
|
|
35
35
|
},
|
|
36
36
|
body: s
|
|
37
37
|
});
|
|
38
|
-
if (i({ function: `SimplePay/makeRequest/${
|
|
38
|
+
if (i({ function: `SimplePay/makeRequest/${n}`, response: o }), !o.ok)
|
|
39
39
|
throw new Error(`SimplePay API error: ${o.status}`);
|
|
40
40
|
const u = o.headers.get("Signature");
|
|
41
|
-
if (i({ function: `SimplePay/makeRequest/${
|
|
41
|
+
if (i({ function: `SimplePay/makeRequest/${n}`, responseSignature: u }), !u)
|
|
42
42
|
throw new Error("Missing response signature");
|
|
43
|
-
const
|
|
44
|
-
if (i({ function: `SimplePay/makeRequest/${
|
|
45
|
-
throw new Error(`SimplePay API error: ${
|
|
46
|
-
if (!
|
|
43
|
+
const a = await o.text(), R = JSON.parse(a);
|
|
44
|
+
if (i({ function: `SimplePay/makeRequest/${n}`, responseText: a, responseJSON: R }), R.errorCodes)
|
|
45
|
+
throw new Error(`SimplePay API error: ${R.errorCodes}`);
|
|
46
|
+
if (!d(a, u, r))
|
|
47
47
|
throw new Error("Invalid response signature");
|
|
48
|
-
return
|
|
48
|
+
return R;
|
|
49
49
|
} catch (o) {
|
|
50
50
|
throw o;
|
|
51
51
|
}
|
|
52
|
-
},
|
|
53
|
-
i({ function: "SimplePay/getPaymentResponse", r: e, signature:
|
|
54
|
-
const r = Buffer.from(e, "base64").toString("utf-8"),
|
|
55
|
-
if (!
|
|
56
|
-
throw i({ function: "SimplePay/getPaymentResponse", rDecoded: r, signature:
|
|
52
|
+
}, w = (e, t) => {
|
|
53
|
+
i({ function: "SimplePay/getPaymentResponse", r: e, signature: t }), t = decodeURIComponent(t);
|
|
54
|
+
const r = Buffer.from(e, "base64").toString("utf-8"), n = JSON.parse(r), s = I(n.m), { MERCHANT_KEY: c } = E(s);
|
|
55
|
+
if (!d(r, t, c || ""))
|
|
56
|
+
throw i({ function: "SimplePay/getPaymentResponse", rDecoded: r, signature: t }), new Error("Invalid response signature");
|
|
57
57
|
const o = JSON.parse(r);
|
|
58
58
|
return {
|
|
59
59
|
responseCode: o.r,
|
|
@@ -63,10 +63,22 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
|
|
|
63
63
|
orderRef: o.o,
|
|
64
64
|
tokens: o.tokens
|
|
65
65
|
};
|
|
66
|
-
},
|
|
66
|
+
}, U = (e, t, r) => {
|
|
67
|
+
if (i({ function: "SimplePay/handleIpnRequest", ipnBody: e, incomingSignature: t }), !d(e, t, r))
|
|
68
|
+
throw new Error("Invalid IPN request signature");
|
|
69
|
+
JSON.parse(e);
|
|
70
|
+
const n = l(/* @__PURE__ */ new Date()), s = e.trim();
|
|
71
|
+
if (!s.endsWith("}"))
|
|
72
|
+
throw new Error("Invalid IPN body format");
|
|
73
|
+
const c = s.lastIndexOf("}"), o = s.substring(0, c).trim(), u = `"receiveDate":"${n}"`, a = o.length > 1 ? `${o},${u}}` : `{${u}}`, R = m(a, r);
|
|
74
|
+
return i({ function: "SimplePay/handleIpnRequest", responseBody: a, responseSignature: R }), {
|
|
75
|
+
responseBody: a,
|
|
76
|
+
signature: R
|
|
77
|
+
};
|
|
78
|
+
}, L = async (e, t = {}) => {
|
|
67
79
|
i({ function: "SimplePay/startPayment", paymentData: e });
|
|
68
|
-
const r = e.currency || "HUF", { MERCHANT_KEY:
|
|
69
|
-
if (i({ function: "SimplePay/startPayment", MERCHANT_KEY:
|
|
80
|
+
const r = e.currency || "HUF", { MERCHANT_KEY: n, MERCHANT_ID: s, API_URL_PAYMENT: c, SDK_VERSION: o } = E(r);
|
|
81
|
+
if (i({ function: "SimplePay/startPayment", MERCHANT_KEY: n, MERCHANT_ID: s, API_URL_PAYMENT: c }), !n || !s)
|
|
70
82
|
throw new Error(`Missing SimplePay configuration for ${r}`);
|
|
71
83
|
const u = {
|
|
72
84
|
salt: S.randomBytes(16).toString("hex"),
|
|
@@ -78,48 +90,48 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
|
|
|
78
90
|
sdkVersion: o,
|
|
79
91
|
methods: [e.method || "CARD"],
|
|
80
92
|
total: String(e.total),
|
|
81
|
-
timeout:
|
|
82
|
-
url:
|
|
93
|
+
timeout: l(new Date(Date.now() + 1800 * 1e3)),
|
|
94
|
+
url: t.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
|
|
83
95
|
invoice: e.invoice
|
|
84
96
|
};
|
|
85
|
-
return p(c, u,
|
|
86
|
-
}, y = 6, C = new Date(Date.now() + y * 30 * 24 * 60 * 60 * 1e3),
|
|
97
|
+
return p(c, u, n);
|
|
98
|
+
}, y = 6, C = new Date(Date.now() + y * 30 * 24 * 60 * 60 * 1e3), N = 12e3, M = 3, H = async (e) => {
|
|
87
99
|
i({ function: "SimplePay/startRecurringPayment", paymentData: e });
|
|
88
|
-
const
|
|
89
|
-
if (i({ function: "SimplePay/startRecurringPayment", MERCHANT_KEY: r, MERCHANT_ID:
|
|
90
|
-
throw new Error(`Missing SimplePay configuration for ${
|
|
100
|
+
const t = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: n, API_URL_PAYMENT: s, SDK_VERSION: c } = E(t);
|
|
101
|
+
if (i({ function: "SimplePay/startRecurringPayment", MERCHANT_KEY: r, MERCHANT_ID: n, API_URL_PAYMENT: s }), !r || !n)
|
|
102
|
+
throw new Error(`Missing SimplePay configuration for ${t}`);
|
|
91
103
|
const o = {
|
|
92
104
|
salt: S.randomBytes(16).toString("hex"),
|
|
93
|
-
merchant:
|
|
105
|
+
merchant: n,
|
|
94
106
|
orderRef: e.orderRef,
|
|
95
|
-
currency:
|
|
107
|
+
currency: t.replace("_SZEP", ""),
|
|
96
108
|
customer: e.customer,
|
|
97
109
|
customerEmail: e.customerEmail,
|
|
98
110
|
language: e.language || "HU",
|
|
99
111
|
sdkVersion: c,
|
|
100
112
|
methods: ["CARD"],
|
|
101
113
|
recurring: {
|
|
102
|
-
times: e.recurring.times ||
|
|
103
|
-
until: e.recurring.until ||
|
|
104
|
-
maxAmount: e.recurring.maxAmount ||
|
|
114
|
+
times: e.recurring.times || M,
|
|
115
|
+
until: e.recurring.until || l(C),
|
|
116
|
+
maxAmount: e.recurring.maxAmount || N
|
|
105
117
|
},
|
|
106
118
|
threeDSReqAuthMethod: "02",
|
|
107
119
|
total: String(e.total),
|
|
108
|
-
timeout:
|
|
120
|
+
timeout: l(new Date(Date.now() + 1800 * 1e3)),
|
|
109
121
|
url: process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
|
|
110
122
|
invoice: e.invoice
|
|
111
123
|
};
|
|
112
|
-
return
|
|
113
|
-
},
|
|
124
|
+
return f(s, o, r);
|
|
125
|
+
}, v = async (e) => {
|
|
114
126
|
i({ function: "SimplePay/startTokenPayment", paymentData: e });
|
|
115
|
-
const
|
|
116
|
-
if (i({ function: "SimplePay/startTokenPayment", MERCHANT_KEY: r, MERCHANT_ID:
|
|
117
|
-
throw new Error(`Missing SimplePay configuration for ${
|
|
127
|
+
const t = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: n, API_URL_RECURRING: s, SDK_VERSION: c } = E(t);
|
|
128
|
+
if (i({ function: "SimplePay/startTokenPayment", MERCHANT_KEY: r, MERCHANT_ID: n, API_URL_RECURRING: s }), !r || !n)
|
|
129
|
+
throw new Error(`Missing SimplePay configuration for ${t}`);
|
|
118
130
|
const o = {
|
|
119
131
|
salt: S.randomBytes(16).toString("hex"),
|
|
120
|
-
merchant:
|
|
132
|
+
merchant: n,
|
|
121
133
|
orderRef: e.orderRef,
|
|
122
|
-
currency:
|
|
134
|
+
currency: t.replace("_SZEP", ""),
|
|
123
135
|
customer: e.customer,
|
|
124
136
|
customerEmail: e.customerEmail,
|
|
125
137
|
language: e.language || "HU",
|
|
@@ -129,32 +141,33 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
|
|
|
129
141
|
type: "MIT",
|
|
130
142
|
threeDSReqAuthMethod: "02",
|
|
131
143
|
total: String(e.total),
|
|
132
|
-
timeout:
|
|
144
|
+
timeout: l(new Date(Date.now() + 1800 * 1e3)),
|
|
133
145
|
url: process.env.SIMPLEPAY_REDIRECT_URL || "http://recurring.url.to.redirect",
|
|
134
146
|
invoice: e.invoice
|
|
135
147
|
};
|
|
136
|
-
return
|
|
148
|
+
return A(s, o, r);
|
|
137
149
|
}, k = async (e) => {
|
|
138
150
|
i({ function: "SimplePay/cancelCard", cardId: e });
|
|
139
|
-
const { API_URL_CARD_CANCEL:
|
|
140
|
-
if (!r || !
|
|
151
|
+
const { API_URL_CARD_CANCEL: t, MERCHANT_KEY: r, MERCHANT_ID: n, SDK_VERSION: s } = E("HUF");
|
|
152
|
+
if (!r || !n)
|
|
141
153
|
throw new Error("Missing SimplePay configuration for HUF");
|
|
142
154
|
const c = {
|
|
143
155
|
salt: S.randomBytes(16).toString("hex"),
|
|
144
156
|
cardId: e,
|
|
145
|
-
merchant:
|
|
157
|
+
merchant: n,
|
|
146
158
|
sdkVersion: s
|
|
147
159
|
};
|
|
148
|
-
return h(
|
|
160
|
+
return h(t, c, r);
|
|
149
161
|
};
|
|
150
162
|
export {
|
|
151
163
|
k as cancelCard,
|
|
152
|
-
|
|
164
|
+
d as checkSignature,
|
|
153
165
|
m as generateSignature,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
L as
|
|
157
|
-
H as
|
|
158
|
-
|
|
166
|
+
w as getPaymentResponse,
|
|
167
|
+
U as handleIpnRequest,
|
|
168
|
+
L as startPayment,
|
|
169
|
+
H as startRecurringPayment,
|
|
170
|
+
v as startTokenPayment,
|
|
171
|
+
l as toISO8601DateString
|
|
159
172
|
};
|
|
160
173
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/types.ts","../src/utils.ts","../src/oneTime.ts","../src/recurring.ts"],"sourcesContent":["export type PaymentMethod = 'CARD' | 'WIRE'\n\nexport const CURRENCIES = ['HUF', 'HUF_SZEP', 'EUR', 'USD'] as const\nexport type Currency = typeof CURRENCIES[number]\n\nexport const LANGUAGES = [\n 'AR', // Arabic\n 'BG', // Bulgarian\n 'CS', // Czech\n 'DE', // German\n 'EN', // English\n 'ES', // Spanish\n 'FR', // French\n 'IT', // Italian\n 'HR', // Croatian\n 'HU', // Hungarian\n 'PL', // Polish\n 'RO', // Romanian\n 'RU', // Russian\n 'SK', // Slovak\n 'TR', // Turkish\n 'ZH', // Chinese\n] as const\nexport type Language = typeof LANGUAGES[number]\n\nexport interface PaymentData {\n orderRef: string\n total: number | string\n customerEmail: string\n currency?: Currency\n language?: Language\n method?: PaymentMethod\n invoice?: {\n name: string\n country: string\n state: string\n city: string\n zip: string\n address: string\n address2?: string\n phone?: string\n }\n}\n\nexport interface PaymentConfig {\n redirectUrl?: string\n}\n\nexport type ISO8601DateString = string\nexport interface Recurring {\n times: number,\n until: ISO8601DateString,\n maxAmount: number\n}\nexport interface RecurringPaymentData extends PaymentData {\n customer: string,\n recurring: Recurring\n}\n\nexport interface TokenPaymentData extends Omit<PaymentData, 'method'> {\n method: 'CARD',\n customer: string,\n token: string\n}\n\nexport interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {\n total: string\n salt: string\n merchant: string\n sdkVersion: string\n methods: PaymentMethod[]\n timeout: string\n url: string\n}\n\nexport interface SimplePayRecurringRequestBody extends SimplePayRequestBody {\n customer: string\n recurring: Recurring\n threeDSReqAuthMethod: '02' // only registered users can use this\n}\n\nexport interface SimplePayTokenRequestBody extends SimplePayRequestBody {\n customer: string\n token: string\n threeDSReqAuthMethod: '02' // only registered users can use this\n type: 'MIT' // Merchant Initiated Transaction\n}\n\nexport interface SimplePayCancelCardRequestBody {\n salt: string\n cardId: string\n merchant: string\n sdkVersion: string\n}\n\nexport interface SimplePayResponse {\n salt: string\n merchant: string\n orderRef: string\n currency: Currency\n transactionId: string\n timeout: ISO8601DateString\n total: string\n paymentUrl: string\n errorCodes?: string[]\n}\n\nexport interface SimplePayRecurringResponse extends SimplePayResponse {\n tokens: string[]\n}\n\nexport interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> { }\n\nexport interface SimplePayCancelCardResponse {\n salt: string\n merchant: string\n cardId: string\n status: 'DISABLED'\n expiry: string\n}\n\nexport type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL'\n\nexport interface SimplePayAPIResult {\n r: number // response code\n t: string // transaction id\n e: SimplePayEvents // event\n m: string // merchant id\n o: string // order id\n}\n\nexport interface SimplePayResult {\n responseCode: number,\n transactionId: string,\n event: SimplePayEvents,\n merchantId: string,\n orderRef: string,\n tokens?: string[],\n}\n","import crypto from 'crypto'\nimport { CURRENCIES, Currency, ISO8601DateString, SimplePayAPIResult, SimplePayCancelCardRequestBody, SimplePayCancelCardResponse, SimplePayRecurringRequestBody, SimplePayRecurringResponse, SimplePayRequestBody, SimplePayResponse, SimplePayResult, SimplePayTokenRequestBody, SimplePayTokenResponse } from \"./types\"\n\nexport const simplepayLogger = (...args: any[]) => {\n if (process.env.SIMPLEPAY_LOGGER !== 'true') {\n return\n }\n\n console.log('👉 ', ...args)\n}\n\nexport const getSimplePayConfig = (currency: Currency) => {\n if (!CURRENCIES.includes(currency)) {\n throw new Error(`Unsupported currency: ${currency}`)\n }\n\n const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'\n const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'\n const SDK_VERSION = 'SimplePay_Rrd_0.11.0'\n const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]\n const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]\n\n const API_URL = process.env.SIMPLEPAY_PRODUCTION === 'true' ? SIMPLEPAY_API_URL : SIMPLEPAY_SANDBOX_URL\n const API_URL_PAYMENT = API_URL + '/start'\n const API_URL_RECURRING = API_URL + '/dorecurring'\n const API_URL_CARD_CANCEL = API_URL + '/cardcancel'\n return {\n MERCHANT_KEY,\n MERCHANT_ID,\n API_URL_PAYMENT,\n API_URL_RECURRING,\n API_URL_CARD_CANCEL,\n SDK_VERSION\n }\n}\n\n// escaping slashes for the request body to prevent strange SimplePay API errors (eg Missing Signature)\nexport const prepareRequestBody = (body: any) =>\n JSON.stringify(body).replace(/\\//g, '\\\\/')\n\nexport const generateSignature = (body: string, merchantKey: string) => {\n const hmac = crypto.createHmac('sha384', merchantKey.trim())\n hmac.update(body, 'utf8')\n return hmac.digest('base64')\n}\n\nexport const checkSignature = (responseText: string, signature: string, merchantKey: string) =>\n signature === generateSignature(responseText, merchantKey)\n\nexport const toISO8601DateString = (date: Date): ISO8601DateString => date.toISOString().replace(/\\.\\d{3}Z$/, '+00:00')\n\nexport const getCurrencyFromMerchantId = (merchantId: string) => {\n const currency = Object.entries(process.env)\n .find(([key, value]) =>\n key.startsWith('SIMPLEPAY_MERCHANT_ID_') && value === merchantId\n )?.[0]?.replace('SIMPLEPAY_MERCHANT_ID_', '') as Currency\n\n if (!currency) {\n throw new Error(`Merchant id not found in the environment: ${merchantId}`)\n }\n\n return currency\n}\n\nexport const makeSimplePayRequest = async (apiUrl: string, requestBody: SimplePayRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'oneTime') as Promise<SimplePayResponse>\n}\n\nexport const makeSimplePayRecurringRequest = async (apiUrl: string, requestBody: SimplePayRecurringRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'recurring') as Promise<SimplePayRecurringResponse>\n}\n\nexport const makeSimplePayTokenRequest = async (apiUrl: string, requestBody: SimplePayTokenRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'token') as Promise<SimplePayTokenResponse>\n}\n\nexport const makeSimplePayCancelCardRequest = async (apiUrl: string, requestBody: SimplePayCancelCardRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'cancelCard') as Promise<SimplePayCancelCardResponse>\n}\n\nconst makeRequest = async (apiUrl: string, requestBody: SimplePayRequestBody | SimplePayRecurringRequestBody | SimplePayTokenRequestBody | SimplePayCancelCardRequestBody, merchantKey: string, type: 'oneTime' | 'recurring' | 'token' | 'cancelCard') => {\n const bodyString = prepareRequestBody(requestBody)\n const signature = generateSignature(bodyString, merchantKey)\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, bodyString, signature })\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Signature': signature,\n },\n body: bodyString,\n })\n\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, response })\n\n if (!response.ok) {\n throw new Error(`SimplePay API error: ${response.status}`)\n }\n\n const responseSignature = response.headers.get('Signature')\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseSignature })\n if (!responseSignature) {\n throw new Error('Missing response signature')\n }\n\n const responseText = await response.text()\n const responseJSON = JSON.parse(responseText) as { errorCodes?: string[] }\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseText, responseJSON })\n\n if (responseJSON.errorCodes) {\n throw new Error(`SimplePay API error: ${responseJSON.errorCodes}`)\n }\n\n if (!checkSignature(responseText, responseSignature, merchantKey)) {\n throw new Error('Invalid response signature')\n }\n\n return responseJSON\n\n } catch (error) {\n throw error\n }\n}\n\nexport const getPaymentResponse = (r: string, signature: string) => {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', r, signature })\n signature = decodeURIComponent(signature)\n const rDecoded = Buffer.from(r, 'base64').toString('utf-8')\n const rDecodedJSON = JSON.parse(rDecoded) as SimplePayAPIResult\n const currency = getCurrencyFromMerchantId(rDecodedJSON.m)\n const { MERCHANT_KEY } = getSimplePayConfig(currency as Currency)\n\n if (!checkSignature(rDecoded, signature, MERCHANT_KEY || '')) {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', rDecoded, signature })\n throw new Error('Invalid response signature')\n }\n\n const responseJson = JSON.parse(rDecoded)\n const response: SimplePayResult = {\n responseCode: responseJson.r,\n transactionId: responseJson.t,\n event: responseJson.e,\n merchantId: responseJson.m,\n orderRef: responseJson.o,\n tokens: responseJson.tokens,\n }\n\n return response\n}\n","import crypto from 'crypto'\nimport { Currency, PaymentConfig, PaymentData, SimplePayRequestBody } from './types'\nimport { simplepayLogger, getSimplePayConfig, toISO8601DateString, makeSimplePayRequest } from './utils'\n\nconst startPayment = async (paymentData: PaymentData, config: PaymentConfig = {}) => {\n simplepayLogger({ function: 'SimplePay/startPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: [paymentData.method || 'CARD'],\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: config.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nexport { startPayment }\n","import crypto from 'crypto'\nimport { SimplePayRecurringRequestBody, RecurringPaymentData, TokenPaymentData, SimplePayTokenRequestBody, SimplePayCancelCardRequestBody, Currency} from './types'\nimport { getSimplePayConfig, simplepayLogger, toISO8601DateString, makeSimplePayTokenRequest, makeSimplePayRecurringRequest, makeSimplePayRequest, makeSimplePayCancelCardRequest} from './utils'\n\nconst INTERVAL_IN_MONTHS = 6\nconst DEFAULT_UNTIL = new Date(Date.now() + INTERVAL_IN_MONTHS * 30 * 24 * 60 * 60 * 1000)\nconst DEFAULT_MAX_AMOUNT = 12000\nconst DEFAULT_TIMES = 3\n\nconst startRecurringPayment = async (paymentData: RecurringPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRecurringRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n recurring: {\n times: paymentData.recurring.times || DEFAULT_TIMES,\n until: paymentData.recurring.until || toISO8601DateString(DEFAULT_UNTIL),\n maxAmount: paymentData.recurring.maxAmount || DEFAULT_MAX_AMOUNT\n },\n threeDSReqAuthMethod: '02', \n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRecurringRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nconst startTokenPayment = async (paymentData: TokenPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startTokenPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startTokenPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayTokenRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n token: paymentData.token,\n type: 'MIT',\n threeDSReqAuthMethod: '02',\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://recurring.url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayTokenRequest(API_URL_RECURRING, requestBody, MERCHANT_KEY)\n}\n\nconst cancelCard = async (cardId: string) => {\n simplepayLogger({ function: 'SimplePay/cancelCard', cardId })\n const {API_URL_CARD_CANCEL, MERCHANT_KEY, MERCHANT_ID, SDK_VERSION} = getSimplePayConfig('HUF')\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for HUF`)\n }\n\n const requestBody: SimplePayCancelCardRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n cardId,\n merchant: MERCHANT_ID,\n sdkVersion: SDK_VERSION,\n }\n return makeSimplePayCancelCardRequest(API_URL_CARD_CANCEL, requestBody, MERCHANT_KEY)\n}\n\nexport { startRecurringPayment, startTokenPayment , cancelCard}\n"],"names":["CURRENCIES","simplepayLogger","args","getSimplePayConfig","currency","SIMPLEPAY_API_URL","SIMPLEPAY_SANDBOX_URL","SDK_VERSION","MERCHANT_KEY","MERCHANT_ID","API_URL","API_URL_PAYMENT","API_URL_RECURRING","API_URL_CARD_CANCEL","prepareRequestBody","body","generateSignature","merchantKey","hmac","crypto","checkSignature","responseText","signature","toISO8601DateString","date","getCurrencyFromMerchantId","merchantId","key","value","makeSimplePayRequest","apiUrl","requestBody","makeRequest","makeSimplePayRecurringRequest","makeSimplePayTokenRequest","makeSimplePayCancelCardRequest","type","bodyString","response","responseSignature","responseJSON","error","getPaymentResponse","r","rDecoded","rDecodedJSON","responseJson","startPayment","paymentData","config","INTERVAL_IN_MONTHS","DEFAULT_UNTIL","DEFAULT_MAX_AMOUNT","DEFAULT_TIMES","startRecurringPayment","startTokenPayment","cancelCard","cardId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/types.ts","../src/utils.ts","../src/oneTime.ts","../src/recurring.ts"],"sourcesContent":["export type PaymentMethod = 'CARD' | 'WIRE'\n\nexport const CURRENCIES = ['HUF', 'HUF_SZEP', 'EUR', 'USD'] as const\nexport type Currency = typeof CURRENCIES[number]\n\nexport const LANGUAGES = [\n 'AR', // Arabic\n 'BG', // Bulgarian\n 'CS', // Czech\n 'DE', // German\n 'EN', // English\n 'ES', // Spanish\n 'FR', // French\n 'IT', // Italian\n 'HR', // Croatian\n 'HU', // Hungarian\n 'PL', // Polish\n 'RO', // Romanian\n 'RU', // Russian\n 'SK', // Slovak\n 'TR', // Turkish\n 'ZH', // Chinese\n] as const\nexport type Language = typeof LANGUAGES[number]\n\nexport interface PaymentData {\n orderRef: string\n total: number | string\n customerEmail: string\n currency?: Currency\n language?: Language\n method?: PaymentMethod\n invoice?: {\n name: string\n country: string\n state: string\n city: string\n zip: string\n address: string\n address2?: string\n phone?: string\n }\n}\n\nexport interface PaymentConfig {\n redirectUrl?: string\n}\n\nexport type ISO8601DateString = string\nexport interface Recurring {\n times: number,\n until: ISO8601DateString,\n maxAmount: number\n}\nexport interface RecurringPaymentData extends PaymentData {\n customer: string,\n recurring: Recurring\n}\n\nexport interface TokenPaymentData extends Omit<PaymentData, 'method'> {\n method: 'CARD',\n customer: string,\n token: string\n}\n\nexport interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {\n total: string\n salt: string\n merchant: string\n sdkVersion: string\n methods: PaymentMethod[]\n timeout: string\n url: string\n}\n\nexport interface SimplePayRecurringRequestBody extends SimplePayRequestBody {\n customer: string\n recurring: Recurring\n threeDSReqAuthMethod: '02' // only registered users can use this\n}\n\nexport interface SimplePayTokenRequestBody extends SimplePayRequestBody {\n customer: string\n token: string\n threeDSReqAuthMethod: '02' // only registered users can use this\n type: 'MIT' // Merchant Initiated Transaction\n}\n\nexport interface SimplePayCancelCardRequestBody {\n salt: string\n cardId: string\n merchant: string\n sdkVersion: string\n}\n\nexport interface SimplePayResponse {\n salt: string\n merchant: string\n orderRef: string\n currency: Currency\n transactionId: string\n timeout: ISO8601DateString\n total: string\n paymentUrl: string\n errorCodes?: string[]\n}\n\nexport interface SimplePayRecurringResponse extends SimplePayResponse {\n tokens: string[]\n}\n\nexport interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> { }\n\nexport interface SimplePayCancelCardResponse {\n salt: string\n merchant: string\n cardId: string\n status: 'DISABLED'\n expiry: string\n}\n\nexport type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL'\n\nexport interface SimplePayAPIResult {\n r: number // response code\n t: string // transaction id\n e: SimplePayEvents // event\n m: string // merchant id\n o: string // order id\n}\n\nexport interface SimplePayResult {\n responseCode: number,\n transactionId: string,\n event: SimplePayEvents,\n merchantId: string,\n orderRef: string,\n tokens?: string[],\n}\n","import crypto from 'crypto'\nimport { CURRENCIES, Currency, ISO8601DateString, SimplePayAPIResult, SimplePayCancelCardRequestBody, SimplePayCancelCardResponse, SimplePayRecurringRequestBody, SimplePayRecurringResponse, SimplePayRequestBody, SimplePayResponse, SimplePayResult, SimplePayTokenRequestBody, SimplePayTokenResponse } from \"./types\"\n\nexport const simplepayLogger = (...args: any[]) => {\n if (process.env.SIMPLEPAY_LOGGER !== 'true') {\n return\n }\n\n console.log('👉 ', ...args)\n}\n\nexport const getSimplePayConfig = (currency: Currency) => {\n if (!CURRENCIES.includes(currency)) {\n throw new Error(`Unsupported currency: ${currency}`)\n }\n\n const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'\n const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'\n const SDK_VERSION = 'SimplePay_Rrd_0.11.2'\n const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]\n const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]\n\n const API_URL = process.env.SIMPLEPAY_PRODUCTION === 'true' ? SIMPLEPAY_API_URL : SIMPLEPAY_SANDBOX_URL\n const API_URL_PAYMENT = API_URL + '/start'\n const API_URL_RECURRING = API_URL + '/dorecurring'\n const API_URL_CARD_CANCEL = API_URL + '/cardcancel'\n return {\n MERCHANT_KEY,\n MERCHANT_ID,\n API_URL_PAYMENT,\n API_URL_RECURRING,\n API_URL_CARD_CANCEL,\n SDK_VERSION\n }\n}\n\n// escaping slashes for the request body to prevent strange SimplePay API errors (eg Missing Signature)\nexport const prepareRequestBody = (body: any) =>\n JSON.stringify(body).replace(/\\//g, '\\\\/')\n\nexport const generateSignature = (body: string, merchantKey: string) => {\n const hmac = crypto.createHmac('sha384', merchantKey.trim())\n hmac.update(body, 'utf8')\n return hmac.digest('base64')\n}\n\nexport const checkSignature = (responseText: string, signature: string, merchantKey: string) =>\n signature === generateSignature(responseText, merchantKey)\n\nexport const toISO8601DateString = (date: Date): ISO8601DateString => date.toISOString().replace(/\\.\\d{3}Z$/, '+00:00')\n\nexport const getCurrencyFromMerchantId = (merchantId: string) => {\n const currency = Object.entries(process.env)\n .find(([key, value]) =>\n key.startsWith('SIMPLEPAY_MERCHANT_ID_') && value === merchantId\n )?.[0]?.replace('SIMPLEPAY_MERCHANT_ID_', '') as Currency\n\n if (!currency) {\n throw new Error(`Merchant id not found in the environment: ${merchantId}`)\n }\n\n return currency\n}\n\nexport const makeSimplePayRequest = async (apiUrl: string, requestBody: SimplePayRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'oneTime') as Promise<SimplePayResponse>\n}\n\nexport const makeSimplePayRecurringRequest = async (apiUrl: string, requestBody: SimplePayRecurringRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'recurring') as Promise<SimplePayRecurringResponse>\n}\n\nexport const makeSimplePayTokenRequest = async (apiUrl: string, requestBody: SimplePayTokenRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'token') as Promise<SimplePayTokenResponse>\n}\n\nexport const makeSimplePayCancelCardRequest = async (apiUrl: string, requestBody: SimplePayCancelCardRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'cancelCard') as Promise<SimplePayCancelCardResponse>\n}\n\nconst makeRequest = async (apiUrl: string, requestBody: SimplePayRequestBody | SimplePayRecurringRequestBody | SimplePayTokenRequestBody | SimplePayCancelCardRequestBody, merchantKey: string, type: 'oneTime' | 'recurring' | 'token' | 'cancelCard') => {\n const bodyString = prepareRequestBody(requestBody)\n const signature = generateSignature(bodyString, merchantKey)\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, bodyString, signature })\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Signature': signature,\n },\n body: bodyString,\n })\n\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, response })\n\n if (!response.ok) {\n throw new Error(`SimplePay API error: ${response.status}`)\n }\n\n const responseSignature = response.headers.get('Signature')\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseSignature })\n if (!responseSignature) {\n throw new Error('Missing response signature')\n }\n\n const responseText = await response.text()\n const responseJSON = JSON.parse(responseText) as { errorCodes?: string[] }\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseText, responseJSON })\n\n if (responseJSON.errorCodes) {\n throw new Error(`SimplePay API error: ${responseJSON.errorCodes}`)\n }\n\n if (!checkSignature(responseText, responseSignature, merchantKey)) {\n throw new Error('Invalid response signature')\n }\n\n return responseJSON\n\n } catch (error) {\n throw error\n }\n}\n\nexport const getPaymentResponse = (r: string, signature: string) => {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', r, signature })\n signature = decodeURIComponent(signature)\n const rDecoded = Buffer.from(r, 'base64').toString('utf-8')\n const rDecodedJSON = JSON.parse(rDecoded) as SimplePayAPIResult\n const currency = getCurrencyFromMerchantId(rDecodedJSON.m)\n const { MERCHANT_KEY } = getSimplePayConfig(currency as Currency)\n\n if (!checkSignature(rDecoded, signature, MERCHANT_KEY || '')) {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', rDecoded, signature })\n throw new Error('Invalid response signature')\n }\n\n const responseJson = JSON.parse(rDecoded)\n const response: SimplePayResult = {\n responseCode: responseJson.r,\n transactionId: responseJson.t,\n event: responseJson.e,\n merchantId: responseJson.m,\n orderRef: responseJson.o,\n tokens: responseJson.tokens,\n }\n\n return response\n}\n\n/**\n * Handles IPN (Instant Payment Notification) request and generates response\n * \n * This function implements the IPN flow according to SimplePay requirements:\n * 1. Validates the incoming signature\n * 2. Adds receiveDate property to the response\n * 3. Generates response signature\n * \n * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)\n * @param incomingSignature - The signature from the 'Signature' HTTP header\n * @param merchantKey - The merchant secret key for signature validation and generation\n * @returns Object containing the response JSON string and signature to send back\n * \n * @example\n * // In your IPN endpoint handler:\n * const ipnBody = await request.text() // Get raw body as string\n * const incomingSignature = request.headers.get('Signature')\n * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)\n * \n * // Send response with HTTP 200 status\n * return new Response(responseBody, {\n * status: 200,\n * headers: {\n * 'Content-Type': 'application/json',\n * 'Signature': signature\n * }\n * })\n */\nexport const handleIpnRequest = (ipnBody: string, incomingSignature: string, merchantKey: string) => {\n simplepayLogger({ function: 'SimplePay/handleIpnRequest', ipnBody, incomingSignature })\n \n // Step 1: Validate incoming signature\n if (!checkSignature(ipnBody, incomingSignature, merchantKey)) {\n throw new Error('Invalid IPN request signature')\n }\n \n // Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order)\n JSON.parse(ipnBody) // Just validate, don't use the result\n \n // Step 3: Add receiveDate to the original JSON string while preserving field order\n // We insert receiveDate before the closing brace to maintain the original field order\n // This ensures the field order in the response matches the original request\n const receiveDate = toISO8601DateString(new Date())\n const trimmedBody = ipnBody.trim()\n \n if (!trimmedBody.endsWith('}')) {\n throw new Error('Invalid IPN body format')\n }\n \n // Remove the closing brace, add receiveDate with comma, then add closing brace back\n const lastBraceIndex = trimmedBody.lastIndexOf('}')\n const beforeLastBrace = trimmedBody.substring(0, lastBraceIndex).trim()\n const receiveDateJson = `\"receiveDate\":\"${receiveDate}\"`\n \n // Add comma before receiveDate if there's already content\n const responseBody = beforeLastBrace.length > 1 \n ? `${beforeLastBrace},${receiveDateJson}}`\n : `{${receiveDateJson}}`\n \n // Step 4: Generate response signature using SHA384 HMAC + Base64\n const responseSignature = generateSignature(responseBody, merchantKey)\n \n simplepayLogger({ function: 'SimplePay/handleIpnRequest', responseBody, responseSignature })\n \n return {\n responseBody,\n signature: responseSignature\n }\n}\n","import crypto from 'crypto'\nimport { Currency, PaymentConfig, PaymentData, SimplePayRequestBody } from './types'\nimport { simplepayLogger, getSimplePayConfig, toISO8601DateString, makeSimplePayRequest } from './utils'\n\nconst startPayment = async (paymentData: PaymentData, config: PaymentConfig = {}) => {\n simplepayLogger({ function: 'SimplePay/startPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: [paymentData.method || 'CARD'],\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: config.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nexport { startPayment }\n","import crypto from 'crypto'\nimport { SimplePayRecurringRequestBody, RecurringPaymentData, TokenPaymentData, SimplePayTokenRequestBody, SimplePayCancelCardRequestBody, Currency} from './types'\nimport { getSimplePayConfig, simplepayLogger, toISO8601DateString, makeSimplePayTokenRequest, makeSimplePayRecurringRequest, makeSimplePayRequest, makeSimplePayCancelCardRequest} from './utils'\n\nconst INTERVAL_IN_MONTHS = 6\nconst DEFAULT_UNTIL = new Date(Date.now() + INTERVAL_IN_MONTHS * 30 * 24 * 60 * 60 * 1000)\nconst DEFAULT_MAX_AMOUNT = 12000\nconst DEFAULT_TIMES = 3\n\nconst startRecurringPayment = async (paymentData: RecurringPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRecurringRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n recurring: {\n times: paymentData.recurring.times || DEFAULT_TIMES,\n until: paymentData.recurring.until || toISO8601DateString(DEFAULT_UNTIL),\n maxAmount: paymentData.recurring.maxAmount || DEFAULT_MAX_AMOUNT\n },\n threeDSReqAuthMethod: '02', \n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRecurringRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nconst startTokenPayment = async (paymentData: TokenPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startTokenPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startTokenPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayTokenRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n token: paymentData.token,\n type: 'MIT',\n threeDSReqAuthMethod: '02',\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://recurring.url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayTokenRequest(API_URL_RECURRING, requestBody, MERCHANT_KEY)\n}\n\nconst cancelCard = async (cardId: string) => {\n simplepayLogger({ function: 'SimplePay/cancelCard', cardId })\n const {API_URL_CARD_CANCEL, MERCHANT_KEY, MERCHANT_ID, SDK_VERSION} = getSimplePayConfig('HUF')\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for HUF`)\n }\n\n const requestBody: SimplePayCancelCardRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n cardId,\n merchant: MERCHANT_ID,\n sdkVersion: SDK_VERSION,\n }\n return makeSimplePayCancelCardRequest(API_URL_CARD_CANCEL, requestBody, MERCHANT_KEY)\n}\n\nexport { startRecurringPayment, startTokenPayment , cancelCard}\n"],"names":["CURRENCIES","simplepayLogger","args","getSimplePayConfig","currency","SIMPLEPAY_API_URL","SIMPLEPAY_SANDBOX_URL","SDK_VERSION","MERCHANT_KEY","MERCHANT_ID","API_URL","API_URL_PAYMENT","API_URL_RECURRING","API_URL_CARD_CANCEL","prepareRequestBody","body","generateSignature","merchantKey","hmac","crypto","checkSignature","responseText","signature","toISO8601DateString","date","getCurrencyFromMerchantId","merchantId","key","value","makeSimplePayRequest","apiUrl","requestBody","makeRequest","makeSimplePayRecurringRequest","makeSimplePayTokenRequest","makeSimplePayCancelCardRequest","type","bodyString","response","responseSignature","responseJSON","error","getPaymentResponse","r","rDecoded","rDecodedJSON","responseJson","handleIpnRequest","ipnBody","incomingSignature","receiveDate","trimmedBody","lastBraceIndex","beforeLastBrace","receiveDateJson","responseBody","startPayment","paymentData","config","INTERVAL_IN_MONTHS","DEFAULT_UNTIL","DEFAULT_MAX_AMOUNT","DEFAULT_TIMES","startRecurringPayment","startTokenPayment","cancelCard","cardId"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { startPayment } from './oneTime'
|
|
2
2
|
import { startRecurringPayment, startTokenPayment, cancelCard} from './recurring'
|
|
3
|
-
import type { Currency, Language, PaymentMethod } from './types'
|
|
4
|
-
import { checkSignature, generateSignature, getPaymentResponse, toISO8601DateString } from './utils'
|
|
3
|
+
import type { Currency, Language, PaymentMethod, SimplePayResponse, SimplePayRecurringResponse, SimplePayTokenResponse, SimplePayCancelCardResponse, SimplePayResult } from './types'
|
|
4
|
+
import { checkSignature, generateSignature, getPaymentResponse, handleIpnRequest, toISO8601DateString } from './utils'
|
|
5
5
|
|
|
6
6
|
export {
|
|
7
7
|
Currency, Language, PaymentMethod,
|
|
8
|
-
|
|
8
|
+
SimplePayResponse, SimplePayRecurringResponse, SimplePayTokenResponse, SimplePayCancelCardResponse, SimplePayResult,
|
|
9
|
+
checkSignature, generateSignature, toISO8601DateString, getPaymentResponse, handleIpnRequest,
|
|
9
10
|
startPayment,
|
|
10
11
|
startRecurringPayment, startTokenPayment, cancelCard
|
|
11
12
|
}
|
package/src/utils.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { Currency } from './types'
|
|
3
|
-
import { checkSignature, generateSignature, getCurrencyFromMerchantId, getSimplePayConfig } from './utils'
|
|
3
|
+
import { checkSignature, generateSignature, getCurrencyFromMerchantId, getSimplePayConfig, handleIpnRequest } from './utils'
|
|
4
4
|
|
|
5
5
|
const setEnv = () => {
|
|
6
6
|
process.env.SIMPLEPAY_MERCHANT_ID_HUF = 'testId'
|
|
@@ -140,4 +140,184 @@ describe('SimplePay Utils Tests', () => {
|
|
|
140
140
|
expect(currencySzep).toBe('HUF_SZEP')
|
|
141
141
|
})
|
|
142
142
|
})
|
|
143
|
+
|
|
144
|
+
describe('IPN Response Signature', () => {
|
|
145
|
+
it('should generate valid signature for IPN response after adding receiveDate', () => {
|
|
146
|
+
setEnv()
|
|
147
|
+
const merchantKey = 'testKey'
|
|
148
|
+
|
|
149
|
+
// Simulate incoming IPN request body (compact JSON, no whitespace)
|
|
150
|
+
const ipnRequestBody = {
|
|
151
|
+
r: 0,
|
|
152
|
+
t: '504233881',
|
|
153
|
+
e: 'SUCCESS',
|
|
154
|
+
m: 'testId',
|
|
155
|
+
o: 'test-order-123'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// JSON.stringify produces compact JSON by default (no whitespace)
|
|
159
|
+
const ipnBodyString = JSON.stringify(ipnRequestBody)
|
|
160
|
+
|
|
161
|
+
// Generate valid incoming signature (as if from SimplePay)
|
|
162
|
+
const incomingSignature = generateSignature(ipnBodyString, merchantKey)
|
|
163
|
+
|
|
164
|
+
// Step 1: Verify incoming signature is valid
|
|
165
|
+
const isIncomingSignatureValid = checkSignature(ipnBodyString, incomingSignature, merchantKey)
|
|
166
|
+
expect(isIncomingSignatureValid).toBeTruthy()
|
|
167
|
+
|
|
168
|
+
// Step 2: Add receiveDate property (as per IPN flow)
|
|
169
|
+
const ipnResponseBody = {
|
|
170
|
+
...ipnRequestBody,
|
|
171
|
+
receiveDate: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// JSON.stringify produces compact JSON (no whitespace) - required by SimplePay
|
|
175
|
+
const ipnResponseBodyString = JSON.stringify(ipnResponseBody)
|
|
176
|
+
|
|
177
|
+
// Step 3: Generate response signature
|
|
178
|
+
const responseSignature = generateSignature(ipnResponseBodyString, merchantKey)
|
|
179
|
+
|
|
180
|
+
// Step 4: Verify the response signature is valid
|
|
181
|
+
const isResponseSignatureValid = checkSignature(ipnResponseBodyString, responseSignature, merchantKey)
|
|
182
|
+
expect(isResponseSignatureValid).toBeTruthy()
|
|
183
|
+
|
|
184
|
+
// Verify response signature is different from incoming signature (since body changed)
|
|
185
|
+
expect(responseSignature).not.toBe(incomingSignature)
|
|
186
|
+
|
|
187
|
+
// Verify JSON is compact (no whitespace)
|
|
188
|
+
expect(ipnResponseBodyString).not.toMatch(/\s/)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle IPN request using handleIpnRequest function', () => {
|
|
192
|
+
setEnv()
|
|
193
|
+
const merchantKey = 'testKey'
|
|
194
|
+
|
|
195
|
+
// Simulate incoming IPN request body (compact JSON, no whitespace)
|
|
196
|
+
// Use a specific field order to test that it's preserved
|
|
197
|
+
const ipnBodyString = '{"r":0,"t":"504233881","e":"SUCCESS","m":"testId","o":"test-order-123"}'
|
|
198
|
+
const incomingSignature = generateSignature(ipnBodyString, merchantKey)
|
|
199
|
+
|
|
200
|
+
// Use handleIpnRequest function
|
|
201
|
+
const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
|
|
202
|
+
|
|
203
|
+
// Verify response contains receiveDate
|
|
204
|
+
const responseData = JSON.parse(responseBody)
|
|
205
|
+
expect(responseData).toHaveProperty('receiveDate')
|
|
206
|
+
expect(responseData.r).toBe(0)
|
|
207
|
+
expect(responseData.t).toBe('504233881')
|
|
208
|
+
expect(responseData.e).toBe('SUCCESS')
|
|
209
|
+
expect(responseData.m).toBe('testId')
|
|
210
|
+
expect(responseData.o).toBe('test-order-123')
|
|
211
|
+
|
|
212
|
+
// Verify response signature is valid
|
|
213
|
+
const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
|
|
214
|
+
expect(isSignatureValid).toBeTruthy()
|
|
215
|
+
|
|
216
|
+
// Verify JSON is compact (no whitespace) - required by SimplePay
|
|
217
|
+
expect(responseBody).not.toMatch(/\s/)
|
|
218
|
+
|
|
219
|
+
// Verify receiveDate is in ISO 8601 format (e.g., 2025-10-06T07:00:34+02:00)
|
|
220
|
+
expect(responseData.receiveDate).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)
|
|
221
|
+
|
|
222
|
+
// Verify field order is preserved: original fields come first, receiveDate is last
|
|
223
|
+
const responseBodyString = responseBody
|
|
224
|
+
expect(responseBodyString).toMatch(/^\{.*"r":0,.*"t":".*",.*"e":".*",.*"m":".*",.*"o":".*",.*"receiveDate":".*"\}$/)
|
|
225
|
+
// Verify receiveDate is the last field before closing brace
|
|
226
|
+
expect(responseBodyString).toMatch(/"receiveDate":"[^"]*"\}$/)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should preserve field order in IPN response', () => {
|
|
230
|
+
setEnv()
|
|
231
|
+
const merchantKey = 'testKey'
|
|
232
|
+
|
|
233
|
+
// Test with a specific field order
|
|
234
|
+
const ipnBodyString = '{"r":0,"t":"123","e":"SUCCESS","m":"merchantId","o":"order123"}'
|
|
235
|
+
const incomingSignature = generateSignature(ipnBodyString, merchantKey)
|
|
236
|
+
|
|
237
|
+
const { responseBody } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
|
|
238
|
+
|
|
239
|
+
// Verify the original fields are in the same order
|
|
240
|
+
const rIndex = responseBody.indexOf('"r":')
|
|
241
|
+
const tIndex = responseBody.indexOf('"t":')
|
|
242
|
+
const eIndex = responseBody.indexOf('"e":')
|
|
243
|
+
const mIndex = responseBody.indexOf('"m":')
|
|
244
|
+
const oIndex = responseBody.indexOf('"o":')
|
|
245
|
+
const receiveDateIndex = responseBody.indexOf('"receiveDate":')
|
|
246
|
+
|
|
247
|
+
// Verify order: r < t < e < m < o < receiveDate
|
|
248
|
+
expect(rIndex).toBeLessThan(tIndex)
|
|
249
|
+
expect(tIndex).toBeLessThan(eIndex)
|
|
250
|
+
expect(eIndex).toBeLessThan(mIndex)
|
|
251
|
+
expect(mIndex).toBeLessThan(oIndex)
|
|
252
|
+
expect(oIndex).toBeLessThan(receiveDateIndex)
|
|
253
|
+
|
|
254
|
+
// Verify receiveDate is the last field
|
|
255
|
+
const lastFieldIndex = Math.max(rIndex, tIndex, eIndex, mIndex, oIndex, receiveDateIndex)
|
|
256
|
+
expect(lastFieldIndex).toBe(receiveDateIndex)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should throw error for invalid incoming IPN signature', () => {
|
|
260
|
+
setEnv()
|
|
261
|
+
const merchantKey = 'testKey'
|
|
262
|
+
|
|
263
|
+
const ipnBodyString = JSON.stringify({ r: 0, t: '123', e: 'SUCCESS', m: 'testId', o: 'test-order' })
|
|
264
|
+
const invalidSignature = 'invalid-signature'
|
|
265
|
+
|
|
266
|
+
expect(() => {
|
|
267
|
+
handleIpnRequest(ipnBodyString, invalidSignature, merchantKey)
|
|
268
|
+
}).toThrow('Invalid IPN request signature')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should generate valid signature for IPN response with different currencies', () => {
|
|
272
|
+
setEnv()
|
|
273
|
+
|
|
274
|
+
const testCases = [
|
|
275
|
+
{ currency: 'HUF' as Currency, merchantKey: 'testKey', merchantId: 'testId' },
|
|
276
|
+
{ currency: 'EUR' as Currency, merchantKey: 'secretEuroKey', merchantId: 'merchantEuroId' },
|
|
277
|
+
{ currency: 'HUF_SZEP' as Currency, merchantKey: 'testKeySzep', merchantId: 'testIdSzep' }
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
testCases.forEach(({ currency, merchantKey, merchantId }) => {
|
|
281
|
+
// Use specific field order to test preservation
|
|
282
|
+
const ipnBodyString = `{"r":0,"t":"504233881","e":"SUCCESS","m":"${merchantId}","o":"test-order-${currency}"}`
|
|
283
|
+
const incomingSignature = generateSignature(ipnBodyString, merchantKey)
|
|
284
|
+
|
|
285
|
+
// Verify incoming signature
|
|
286
|
+
expect(checkSignature(ipnBodyString, incomingSignature, merchantKey)).toBeTruthy()
|
|
287
|
+
|
|
288
|
+
// Use handleIpnRequest function
|
|
289
|
+
const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
|
|
290
|
+
|
|
291
|
+
// Verify response signature
|
|
292
|
+
expect(checkSignature(responseBody, signature, merchantKey)).toBeTruthy()
|
|
293
|
+
|
|
294
|
+
// Verify JSON is compact (no whitespace)
|
|
295
|
+
expect(responseBody).not.toMatch(/\s/)
|
|
296
|
+
|
|
297
|
+
// Verify field order is preserved
|
|
298
|
+
expect(responseBody).toMatch(/^\{.*"r":0,.*"t":".*",.*"e":".*",.*"m":".*",.*"o":".*",.*"receiveDate":".*"\}$/)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should reject IPN response with invalid signature', () => {
|
|
303
|
+
setEnv()
|
|
304
|
+
const merchantKey = 'testKey'
|
|
305
|
+
|
|
306
|
+
const ipnRequestBody = {
|
|
307
|
+
r: 0,
|
|
308
|
+
t: '504233881',
|
|
309
|
+
e: 'SUCCESS',
|
|
310
|
+
m: 'testId',
|
|
311
|
+
o: 'test-order-123',
|
|
312
|
+
receiveDate: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// JSON.stringify produces compact JSON (no whitespace)
|
|
316
|
+
const ipnResponseBodyString = JSON.stringify(ipnRequestBody)
|
|
317
|
+
const invalidSignature = 'invalid-signature'
|
|
318
|
+
|
|
319
|
+
const isValid = checkSignature(ipnResponseBodyString, invalidSignature, merchantKey)
|
|
320
|
+
expect(isValid).toBeFalsy()
|
|
321
|
+
})
|
|
322
|
+
})
|
|
143
323
|
})
|
package/src/utils.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const getSimplePayConfig = (currency: Currency) => {
|
|
|
16
16
|
|
|
17
17
|
const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'
|
|
18
18
|
const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'
|
|
19
|
-
const SDK_VERSION = 'SimplePay_Rrd_0.
|
|
19
|
+
const SDK_VERSION = 'SimplePay_Rrd_0.12.0'
|
|
20
20
|
const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]
|
|
21
21
|
const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]
|
|
22
22
|
|
|
@@ -149,3 +149,73 @@ export const getPaymentResponse = (r: string, signature: string) => {
|
|
|
149
149
|
|
|
150
150
|
return response
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handles IPN (Instant Payment Notification) request and generates response
|
|
155
|
+
*
|
|
156
|
+
* This function implements the IPN flow according to SimplePay requirements:
|
|
157
|
+
* 1. Validates the incoming signature
|
|
158
|
+
* 2. Adds receiveDate property to the response
|
|
159
|
+
* 3. Generates response signature
|
|
160
|
+
*
|
|
161
|
+
* @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
|
|
162
|
+
* @param incomingSignature - The signature from the 'Signature' HTTP header
|
|
163
|
+
* @param merchantKey - The merchant secret key for signature validation and generation
|
|
164
|
+
* @returns Object containing the response JSON string and signature to send back
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // In your IPN endpoint handler:
|
|
168
|
+
* const ipnBody = await request.text() // Get raw body as string
|
|
169
|
+
* const incomingSignature = request.headers.get('Signature')
|
|
170
|
+
* const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
|
|
171
|
+
*
|
|
172
|
+
* // Send response with HTTP 200 status
|
|
173
|
+
* return new Response(responseBody, {
|
|
174
|
+
* status: 200,
|
|
175
|
+
* headers: {
|
|
176
|
+
* 'Content-Type': 'application/json',
|
|
177
|
+
* 'Signature': signature
|
|
178
|
+
* }
|
|
179
|
+
* })
|
|
180
|
+
*/
|
|
181
|
+
export const handleIpnRequest = (ipnBody: string, incomingSignature: string, merchantKey: string) => {
|
|
182
|
+
simplepayLogger({ function: 'SimplePay/handleIpnRequest', ipnBody, incomingSignature })
|
|
183
|
+
|
|
184
|
+
// Step 1: Validate incoming signature
|
|
185
|
+
if (!checkSignature(ipnBody, incomingSignature, merchantKey)) {
|
|
186
|
+
throw new Error('Invalid IPN request signature')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order)
|
|
190
|
+
JSON.parse(ipnBody) // Just validate, don't use the result
|
|
191
|
+
|
|
192
|
+
// Step 3: Add receiveDate to the original JSON string while preserving field order
|
|
193
|
+
// We insert receiveDate before the closing brace to maintain the original field order
|
|
194
|
+
// This ensures the field order in the response matches the original request
|
|
195
|
+
const receiveDate = toISO8601DateString(new Date())
|
|
196
|
+
const trimmedBody = ipnBody.trim()
|
|
197
|
+
|
|
198
|
+
if (!trimmedBody.endsWith('}')) {
|
|
199
|
+
throw new Error('Invalid IPN body format')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remove the closing brace, add receiveDate with comma, then add closing brace back
|
|
203
|
+
const lastBraceIndex = trimmedBody.lastIndexOf('}')
|
|
204
|
+
const beforeLastBrace = trimmedBody.substring(0, lastBraceIndex).trim()
|
|
205
|
+
const receiveDateJson = `"receiveDate":"${receiveDate}"`
|
|
206
|
+
|
|
207
|
+
// Add comma before receiveDate if there's already content
|
|
208
|
+
const responseBody = beforeLastBrace.length > 1
|
|
209
|
+
? `${beforeLastBrace},${receiveDateJson}}`
|
|
210
|
+
: `{${receiveDateJson}}`
|
|
211
|
+
|
|
212
|
+
// Step 4: Generate response signature using SHA384 HMAC + Base64
|
|
213
|
+
const responseSignature = generateSignature(responseBody, merchantKey)
|
|
214
|
+
|
|
215
|
+
simplepayLogger({ function: 'SimplePay/handleIpnRequest', responseBody, responseSignature })
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
responseBody,
|
|
219
|
+
signature: responseSignature
|
|
220
|
+
}
|
|
221
|
+
}
|