simplepay-js-sdk 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,12 +102,19 @@ A SimplePay `POST` kérést küld az IPN URL-re, és válaszolnunk kell rá.
102
102
  import { handleIpnRequest } from 'simplepay-js-sdk'
103
103
 
104
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)
105
+ const ipnBody = await request.text() // A nyers body stringként (FONTOS: használd a .text()-et, ne a JSON.parse()-t)
106
106
  const incomingSignature = request.headers.get('Signature')
107
107
  const { MERCHANT_KEY } = getSimplePayConfig('HUF') // vagy a te valutád
108
108
 
109
109
  const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
110
110
 
111
+ // KRITIKUS: A responseBody-t pontosan úgy küldd el, ahogy visszaadódott, NE módosítsd!
112
+ // NE:
113
+ // - Formázd újra vagy "szépítsd" a JSON-t
114
+ // - Parse-old és stringify-zd újra a JSON-t
115
+ // - Adj hozzá szóközöket vagy formázást
116
+ // Bármilyen módosítás érvényteleníti az aláírást!
117
+
111
118
  // Válasz küldése HTTP 200 státusszal
112
119
  return new Response(responseBody, {
113
120
  status: 200,
package/dist/index.d.ts CHANGED
@@ -15,9 +15,19 @@ export declare const getPaymentResponse: (r: string, signature: string) => Simpl
15
15
  *
16
16
  * This function implements the IPN flow according to SimplePay requirements:
17
17
  * 1. Validates the incoming signature
18
- * 2. Adds receiveDate property to the response
18
+ * 2. Adds receiveDate property to the response (preserving original field order and data types)
19
19
  * 3. Generates response signature
20
20
  *
21
+ * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.
22
+ * Do NOT:
23
+ * - Re-format or pretty-print the JSON
24
+ * - Parse and re-stringify the JSON
25
+ * - Add any whitespace or formatting
26
+ * - Modify the Content-Type header (must be 'application/json')
27
+ *
28
+ * The signature is calculated on the exact string that will be sent in the HTTP body.
29
+ * Any modification to the responseBody will invalidate the signature.
30
+ *
21
31
  * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
22
32
  * @param incomingSignature - The signature from the 'Signature' HTTP header
23
33
  * @param merchantKey - The merchant secret key for signature validation and generation
@@ -25,11 +35,12 @@ export declare const getPaymentResponse: (r: string, signature: string) => Simpl
25
35
  *
26
36
  * @example
27
37
  * // In your IPN endpoint handler:
28
- * const ipnBody = await request.text() // Get raw body as string
38
+ * const ipnBody = await request.text() // Get raw body as string (IMPORTANT: use .text(), not JSON.parse())
29
39
  * const incomingSignature = request.headers.get('Signature')
30
40
  * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
31
41
  *
32
42
  * // Send response with HTTP 200 status
43
+ * // CRITICAL: Send responseBody exactly as returned, do not modify it!
33
44
  * return new Response(responseBody, {
34
45
  * status: 200,
35
46
  * headers: {
package/dist/index.js CHANGED
@@ -1,31 +1,31 @@
1
- import S from "crypto";
2
- const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
1
+ import l from "crypto";
2
+ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
3
3
  process.env.SIMPLEPAY_LOGGER === "true" && console.log("👉 ", ...e);
4
4
  }, E = (e) => {
5
- if (!_.includes(e))
5
+ if (!d.includes(e))
6
6
  throw new Error(`Unsupported currency: ${e}`);
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";
7
+ const n = "https://secure.simplepay.hu/payment/v2", r = "https://sandbox.simplepay.hu/payment/v2", t = "SimplePay_Rrd_0.12.0", s = process.env[`SIMPLEPAY_MERCHANT_KEY_${e}`], c = process.env[`SIMPLEPAY_MERCHANT_ID_${e}`], o = process.env.SIMPLEPAY_PRODUCTION === "true" ? n : r, u = o + "/start", R = o + "/dorecurring", a = 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: a,
13
- API_URL_CARD_CANCEL: R,
14
- SDK_VERSION: n
12
+ API_URL_RECURRING: R,
13
+ API_URL_CARD_CANCEL: a,
14
+ SDK_VERSION: t
15
15
  };
16
- }, g = (e) => JSON.stringify(e).replace(/\//g, "\\/"), m = (e, t) => {
17
- const r = S.createHmac("sha384", t.trim());
16
+ }, g = (e) => JSON.stringify(e).replace(/\//g, "\\/"), m = (e, n) => {
17
+ const r = l.createHmac("sha384", n.trim());
18
18
  return r.update(e, "utf8"), r.digest("base64");
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
19
+ }, _ = (e, n, r) => n === m(e, r), S = (e) => e.toISOString().replace(/\.\d{3}Z$/, "+00:00"), p = (e) => {
20
+ const n = Object.entries(process.env).find(
21
+ ([r, t]) => r.startsWith("SIMPLEPAY_MERCHANT_ID_") && t === e
22
22
  )?.[0]?.replace("SIMPLEPAY_MERCHANT_ID_", "");
23
- if (!t)
23
+ if (!n)
24
24
  throw new Error(`Merchant id not found in the environment: ${e}`);
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 });
25
+ return n;
26
+ }, A = async (e, n, r) => P(e, n, r, "oneTime"), I = async (e, n, r) => P(e, n, r, "recurring"), f = async (e, n, r) => P(e, n, r, "token"), h = async (e, n, r) => P(e, n, r, "cancelCard"), P = async (e, n, r, t) => {
27
+ const s = g(n), c = m(s, r);
28
+ i({ function: `SimplePay/makeRequest/${t}`, bodyString: s, signature: c });
29
29
  try {
30
30
  const o = await fetch(e, {
31
31
  method: "POST",
@@ -35,25 +35,25 @@ const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
35
35
  },
36
36
  body: s
37
37
  });
38
- if (i({ function: `SimplePay/makeRequest/${n}`, response: o }), !o.ok)
38
+ if (i({ function: `SimplePay/makeRequest/${t}`, 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/${n}`, responseSignature: u }), !u)
41
+ if (i({ function: `SimplePay/makeRequest/${t}`, responseSignature: u }), !u)
42
42
  throw new Error("Missing response signature");
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))
43
+ const R = await o.text(), a = JSON.parse(R);
44
+ if (i({ function: `SimplePay/makeRequest/${t}`, responseText: R, responseJSON: a }), a.errorCodes)
45
+ throw new Error(`SimplePay API error: ${a.errorCodes}`);
46
+ if (!_(R, u, r))
47
47
  throw new Error("Invalid response signature");
48
- return R;
48
+ return a;
49
49
  } catch (o) {
50
50
  throw o;
51
51
  }
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");
52
+ }, U = (e, n) => {
53
+ i({ function: "SimplePay/getPaymentResponse", r: e, signature: n }), n = decodeURIComponent(n);
54
+ const r = Buffer.from(e, "base64").toString("utf-8"), t = JSON.parse(r), s = p(t.m), { MERCHANT_KEY: c } = E(s);
55
+ if (!_(r, n, c || ""))
56
+ throw i({ function: "SimplePay/getPaymentResponse", rDecoded: r, signature: n }), new Error("Invalid response signature");
57
57
  const o = JSON.parse(r);
58
58
  return {
59
59
  responseCode: o.r,
@@ -63,25 +63,22 @@ const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
63
63
  orderRef: o.o,
64
64
  tokens: o.tokens
65
65
  };
66
- }, U = (e, t, r) => {
67
- if (i({ function: "SimplePay/handleIpnRequest", ipnBody: e, incomingSignature: t }), !d(e, t, r))
66
+ }, w = (e, n, r) => {
67
+ if (i({ function: "SimplePay/handleIpnRequest", ipnBody: e, incomingSignature: n }), !_(e, n, r))
68
68
  throw new Error("Invalid IPN request signature");
69
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
70
+ const t = S(/* @__PURE__ */ new Date()), s = e.replace("}", `,"receiveDate":"${t}"}`), c = m(s, r);
71
+ return i({ function: "SimplePay/handleIpnRequest", responseBody: s, responseSignature: c }), {
72
+ responseBody: s,
73
+ signature: c
77
74
  };
78
- }, L = async (e, t = {}) => {
75
+ }, L = async (e, n = {}) => {
79
76
  i({ function: "SimplePay/startPayment", paymentData: e });
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)
77
+ const r = e.currency || "HUF", { MERCHANT_KEY: t, MERCHANT_ID: s, API_URL_PAYMENT: c, SDK_VERSION: o } = E(r);
78
+ if (i({ function: "SimplePay/startPayment", MERCHANT_KEY: t, MERCHANT_ID: s, API_URL_PAYMENT: c }), !t || !s)
82
79
  throw new Error(`Missing SimplePay configuration for ${r}`);
83
80
  const u = {
84
- salt: S.randomBytes(16).toString("hex"),
81
+ salt: l.randomBytes(16).toString("hex"),
85
82
  merchant: s,
86
83
  orderRef: e.orderRef,
87
84
  currency: r.replace("_SZEP", ""),
@@ -90,48 +87,48 @@ const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
90
87
  sdkVersion: o,
91
88
  methods: [e.method || "CARD"],
92
89
  total: String(e.total),
93
- timeout: l(new Date(Date.now() + 1800 * 1e3)),
94
- url: t.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
90
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
91
+ url: n.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
95
92
  invoice: e.invoice
96
93
  };
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) => {
94
+ return A(c, u, t);
95
+ }, y = 6, C = new Date(Date.now() + y * 30 * 24 * 60 * 60 * 1e3), M = 12e3, N = 3, H = async (e) => {
99
96
  i({ function: "SimplePay/startRecurringPayment", paymentData: e });
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}`);
97
+ const n = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_PAYMENT: s, SDK_VERSION: c } = E(n);
98
+ if (i({ function: "SimplePay/startRecurringPayment", MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_PAYMENT: s }), !r || !t)
99
+ throw new Error(`Missing SimplePay configuration for ${n}`);
103
100
  const o = {
104
- salt: S.randomBytes(16).toString("hex"),
105
- merchant: n,
101
+ salt: l.randomBytes(16).toString("hex"),
102
+ merchant: t,
106
103
  orderRef: e.orderRef,
107
- currency: t.replace("_SZEP", ""),
104
+ currency: n.replace("_SZEP", ""),
108
105
  customer: e.customer,
109
106
  customerEmail: e.customerEmail,
110
107
  language: e.language || "HU",
111
108
  sdkVersion: c,
112
109
  methods: ["CARD"],
113
110
  recurring: {
114
- times: e.recurring.times || M,
115
- until: e.recurring.until || l(C),
116
- maxAmount: e.recurring.maxAmount || N
111
+ times: e.recurring.times || N,
112
+ until: e.recurring.until || S(C),
113
+ maxAmount: e.recurring.maxAmount || M
117
114
  },
118
115
  threeDSReqAuthMethod: "02",
119
116
  total: String(e.total),
120
- timeout: l(new Date(Date.now() + 1800 * 1e3)),
117
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
121
118
  url: process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
122
119
  invoice: e.invoice
123
120
  };
124
- return f(s, o, r);
125
- }, v = async (e) => {
121
+ return I(s, o, r);
122
+ }, k = async (e) => {
126
123
  i({ function: "SimplePay/startTokenPayment", paymentData: e });
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}`);
124
+ const n = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_RECURRING: s, SDK_VERSION: c } = E(n);
125
+ if (i({ function: "SimplePay/startTokenPayment", MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_RECURRING: s }), !r || !t)
126
+ throw new Error(`Missing SimplePay configuration for ${n}`);
130
127
  const o = {
131
- salt: S.randomBytes(16).toString("hex"),
132
- merchant: n,
128
+ salt: l.randomBytes(16).toString("hex"),
129
+ merchant: t,
133
130
  orderRef: e.orderRef,
134
- currency: t.replace("_SZEP", ""),
131
+ currency: n.replace("_SZEP", ""),
135
132
  customer: e.customer,
136
133
  customerEmail: e.customerEmail,
137
134
  language: e.language || "HU",
@@ -141,33 +138,33 @@ const _ = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
141
138
  type: "MIT",
142
139
  threeDSReqAuthMethod: "02",
143
140
  total: String(e.total),
144
- timeout: l(new Date(Date.now() + 1800 * 1e3)),
141
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
145
142
  url: process.env.SIMPLEPAY_REDIRECT_URL || "http://recurring.url.to.redirect",
146
143
  invoice: e.invoice
147
144
  };
148
- return A(s, o, r);
149
- }, k = async (e) => {
145
+ return f(s, o, r);
146
+ }, v = async (e) => {
150
147
  i({ function: "SimplePay/cancelCard", cardId: e });
151
- const { API_URL_CARD_CANCEL: t, MERCHANT_KEY: r, MERCHANT_ID: n, SDK_VERSION: s } = E("HUF");
152
- if (!r || !n)
148
+ const { API_URL_CARD_CANCEL: n, MERCHANT_KEY: r, MERCHANT_ID: t, SDK_VERSION: s } = E("HUF");
149
+ if (!r || !t)
153
150
  throw new Error("Missing SimplePay configuration for HUF");
154
151
  const c = {
155
- salt: S.randomBytes(16).toString("hex"),
152
+ salt: l.randomBytes(16).toString("hex"),
156
153
  cardId: e,
157
- merchant: n,
154
+ merchant: t,
158
155
  sdkVersion: s
159
156
  };
160
- return h(t, c, r);
157
+ return h(n, c, r);
161
158
  };
162
159
  export {
163
- k as cancelCard,
164
- d as checkSignature,
160
+ v as cancelCard,
161
+ _ as checkSignature,
165
162
  m as generateSignature,
166
- w as getPaymentResponse,
167
- U as handleIpnRequest,
163
+ U as getPaymentResponse,
164
+ w as handleIpnRequest,
168
165
  L as startPayment,
169
166
  H as startRecurringPayment,
170
- v as startTokenPayment,
171
- l as toISO8601DateString
167
+ k as startTokenPayment,
168
+ S as toISO8601DateString
172
169
  };
173
170
  //# 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.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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"}
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.12.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\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 (preserving original field order and data types)\n * 3. Generates response signature\n * \n * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.\n * Do NOT:\n * - Re-format or pretty-print the JSON\n * - Parse and re-stringify the JSON\n * - Add any whitespace or formatting\n * - Modify the Content-Type header (must be 'application/json')\n * \n * The signature is calculated on the exact string that will be sent in the HTTP body.\n * Any modification to the responseBody will invalidate the 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 (IMPORTANT: use .text(), not JSON.parse())\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 * // CRITICAL: Send responseBody exactly as returned, do not modify it!\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 and data types)\n JSON.parse(ipnBody) // Just validate, don't use the result\n \n // Step 3: Add receiveDate to the original JSON string while preserving:\n // - Exact field order\n // - Exact data types (numeric vs string)\n // - No whitespace (compact JSON)\n // \n // Use the same simple approach as the working solution:\n // Replace the closing brace with: ,\"receiveDate\":\"${receiveDate}\"}\n // This preserves the exact original format and field order\n const receiveDate = toISO8601DateString(new Date())\n \n // Simple string replacement: replace the first (and only) closing brace\n // This is the same approach as the working solution\n const responseBody = ipnBody.replace('}', `,\"receiveDate\":\"${receiveDate}\"}`)\n \n // Step 4: Generate response signature using SHA384 HMAC + Base64\n // The signature must be calculated on the exact string that will be sent\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","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplepay-js-sdk",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "A Node.js utility for SimplePay payment integration",
5
5
  "repository": {
6
6
  "type": "git",
package/src/utils.spec.ts CHANGED
@@ -256,6 +256,55 @@ describe('SimplePay Utils Tests', () => {
256
256
  expect(lastFieldIndex).toBe(receiveDateIndex)
257
257
  })
258
258
 
259
+ it('should preserve numeric data types in IPN response', () => {
260
+ setEnv()
261
+ const merchantKey = 'testKey'
262
+
263
+ // Test with numeric transactionId (not string)
264
+ // This is critical: SimplePay sends numeric values, they must stay numeric
265
+ const ipnBodyString = '{"r":0,"t":508163884,"e":"SUCCESS","m":"testId","o":"order123"}'
266
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
267
+
268
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
269
+
270
+ // Verify numeric transactionId stays numeric (no quotes around the number)
271
+ expect(responseBody).toMatch(/"t":508163884,/)
272
+ expect(responseBody).not.toMatch(/"t":"508163884",/)
273
+
274
+ // Verify response signature is valid
275
+ const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
276
+ expect(isSignatureValid).toBeTruthy()
277
+
278
+ // Verify the response can be parsed and numeric value is preserved
279
+ const parsed = JSON.parse(responseBody)
280
+ expect(typeof parsed.t).toBe('number')
281
+ expect(parsed.t).toBe(508163884)
282
+ })
283
+
284
+ it('should preserve string data types in IPN response', () => {
285
+ setEnv()
286
+ const merchantKey = 'testKey'
287
+
288
+ // Test with string transactionId
289
+ const ipnBodyString = '{"r":0,"t":"504233881","e":"SUCCESS","m":"testId","o":"order123"}'
290
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
291
+
292
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
293
+
294
+ // Verify string transactionId stays string (with quotes)
295
+ expect(responseBody).toMatch(/"t":"504233881",/)
296
+ expect(responseBody).not.toMatch(/"t":504233881,/)
297
+
298
+ // Verify response signature is valid
299
+ const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
300
+ expect(isSignatureValid).toBeTruthy()
301
+
302
+ // Verify the response can be parsed and string value is preserved
303
+ const parsed = JSON.parse(responseBody)
304
+ expect(typeof parsed.t).toBe('string')
305
+ expect(parsed.t).toBe('504233881')
306
+ })
307
+
259
308
  it('should throw error for invalid incoming IPN signature', () => {
260
309
  setEnv()
261
310
  const merchantKey = 'testKey'
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.12.0'
19
+ const SDK_VERSION = 'SimplePay_Rrd_0.12.1'
20
20
  const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]
21
21
  const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]
22
22
 
@@ -155,9 +155,19 @@ export const getPaymentResponse = (r: string, signature: string) => {
155
155
  *
156
156
  * This function implements the IPN flow according to SimplePay requirements:
157
157
  * 1. Validates the incoming signature
158
- * 2. Adds receiveDate property to the response
158
+ * 2. Adds receiveDate property to the response (preserving original field order and data types)
159
159
  * 3. Generates response signature
160
160
  *
161
+ * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.
162
+ * Do NOT:
163
+ * - Re-format or pretty-print the JSON
164
+ * - Parse and re-stringify the JSON
165
+ * - Add any whitespace or formatting
166
+ * - Modify the Content-Type header (must be 'application/json')
167
+ *
168
+ * The signature is calculated on the exact string that will be sent in the HTTP body.
169
+ * Any modification to the responseBody will invalidate the signature.
170
+ *
161
171
  * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
162
172
  * @param incomingSignature - The signature from the 'Signature' HTTP header
163
173
  * @param merchantKey - The merchant secret key for signature validation and generation
@@ -165,11 +175,12 @@ export const getPaymentResponse = (r: string, signature: string) => {
165
175
  *
166
176
  * @example
167
177
  * // In your IPN endpoint handler:
168
- * const ipnBody = await request.text() // Get raw body as string
178
+ * const ipnBody = await request.text() // Get raw body as string (IMPORTANT: use .text(), not JSON.parse())
169
179
  * const incomingSignature = request.headers.get('Signature')
170
180
  * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
171
181
  *
172
182
  * // Send response with HTTP 200 status
183
+ * // CRITICAL: Send responseBody exactly as returned, do not modify it!
173
184
  * return new Response(responseBody, {
174
185
  * status: 200,
175
186
  * headers: {
@@ -186,30 +197,25 @@ export const handleIpnRequest = (ipnBody: string, incomingSignature: string, mer
186
197
  throw new Error('Invalid IPN request signature')
187
198
  }
188
199
 
189
- // Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order)
200
+ // Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order and data types)
190
201
  JSON.parse(ipnBody) // Just validate, don't use the result
191
202
 
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
203
+ // Step 3: Add receiveDate to the original JSON string while preserving:
204
+ // - Exact field order
205
+ // - Exact data types (numeric vs string)
206
+ // - No whitespace (compact JSON)
207
+ //
208
+ // Use the same simple approach as the working solution:
209
+ // Replace the closing brace with: ,"receiveDate":"${receiveDate}"}
210
+ // This preserves the exact original format and field order
195
211
  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
212
 
207
- // Add comma before receiveDate if there's already content
208
- const responseBody = beforeLastBrace.length > 1
209
- ? `${beforeLastBrace},${receiveDateJson}}`
210
- : `{${receiveDateJson}}`
213
+ // Simple string replacement: replace the first (and only) closing brace
214
+ // This is the same approach as the working solution
215
+ const responseBody = ipnBody.replace('}', `,"receiveDate":"${receiveDate}"}`)
211
216
 
212
217
  // Step 4: Generate response signature using SHA384 HMAC + Base64
218
+ // The signature must be calculated on the exact string that will be sent
213
219
  const responseSignature = generateSignature(responseBody, merchantKey)
214
220
 
215
221
  simplepayLogger({ function: 'SimplePay/handleIpnRequest', responseBody, responseSignature })