@katorymnd/pawapay-node-sdk 1.1.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.
@@ -0,0 +1,81 @@
1
+ //# Core license validation
2
+
3
+ // const crypto = require("crypto");
4
+ const {
5
+ // PAWAPAY_SDK_LICENSE_DOMAIN, // The domain (e.g., startup-app.com)
6
+ PAWAPAY_SDK_LICENSE_SECRET // Required for signature validation (Base64)
7
+ } = process.env;
8
+
9
+ class LicenseValidator {
10
+ constructor() {
11
+ this.validatedLicenses = new WeakMap();
12
+ this.lastCheck = Date.now();
13
+ }
14
+
15
+ validate(licenseKey) {
16
+ // --- LOG: Initial input ---
17
+ console.log(`[PawaPay License] Validating key: ${licenseKey}`);
18
+
19
+ if (!licenseKey || typeof licenseKey !== "string") {
20
+ return this._fail("Invalid license key format: Key is null, undefined, or not a string");
21
+ }
22
+
23
+ // --- 1. FORMAT VALIDATION: Accept TECH-TIER-XXXX-XXXX-XXXX ---
24
+ // Example: NODE-START-14F6-1053-4CD7
25
+ const regex = /^[A-Z]+-[A-Z]+-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/;
26
+ if (!regex.test(licenseKey)) {
27
+ return this._fail(`License key format mismatch. Expected format like: NODE-START-XXXX-XXXX-XXXX. Got: ${licenseKey}`);
28
+ }
29
+
30
+ // --- 2. SECRET KEY VALIDATION ---
31
+ if (!PAWAPAY_SDK_LICENSE_SECRET) {
32
+ return this._fail("Environment variable PAWAPAY_SDK_LICENSE_SECRET is not set. This is required to validate license signatures.");
33
+ }
34
+
35
+ // --- 3. REPLICATE PHP LOGIC: DECODE SECRET ---
36
+ let secretHexFull;
37
+ try {
38
+ // PHP: base64_decode($clientSecret, true) -> bin2hex(...)
39
+ const secretBuffer = Buffer.from(PAWAPAY_SDK_LICENSE_SECRET, "base64");
40
+ if (secretBuffer.length === 0) throw new Error("Empty secret");
41
+
42
+ secretHexFull = secretBuffer.toString("hex");
43
+ } catch (err) {
44
+ return this._fail(`Failed to decode secret key (Base64): ${err.message}`);
45
+ }
46
+
47
+ // --- 4. EXTRACT SIGNATURES ---
48
+ // PHP Logic: $secretSignature = strtoupper(substr($fullHex, 0, 12));
49
+ const expectedSignature = secretHexFull.substring(0, 12).toUpperCase();
50
+
51
+ // PHP Logic: $licenseSignature = strtoupper($parts[2] . $parts[3] . $parts[4]);
52
+ const parts = licenseKey.split("-");
53
+ const techPrefix = parts[0]; // e.g. NODE
54
+ const tier = parts[1]; // e.g. START, PRO
55
+ const providedSignature = (parts[2] + parts[3] + parts[4]).toUpperCase();
56
+
57
+ console.log(`[PawaPay License] Expected Signature (from Secret): ${expectedSignature}`);
58
+ console.log(`[PawaPay License] Provided Signature (from Key): ${providedSignature}`);
59
+
60
+ // --- 5. COMPARE ---
61
+ if (providedSignature !== expectedSignature) {
62
+ return this._fail("Invalid license signature. Credentials do not match.");
63
+ }
64
+
65
+ // --- SUCCESS ---
66
+ console.log(`[PawaPay License] License key validation SUCCESSFUL for: ${licenseKey}`);
67
+ return {
68
+ valid: true,
69
+ tech: techPrefix,
70
+ tier: tier,
71
+ licenseKey
72
+ };
73
+ }
74
+
75
+ _fail(reason) {
76
+ console.error(`[PawaPay License] ${reason}`);
77
+ return { valid: false, reason };
78
+ }
79
+ }
80
+
81
+ module.exports = new LicenseValidator();
@@ -0,0 +1,317 @@
1
+ /**
2
+ * D:\pawapay\pawapay-node-sdk\src\utils\validator.js
3
+ *
4
+ * A surgical, idiomatic Node.js translation of the original PHP Validator class
5
+ * from Katorymnd\PawaPayIntegration\Utils, updated to use Joi for amount validation,
6
+ * mirroring Symfony's role in the original PHP code.
7
+ *
8
+ * Notes:
9
+ * - Joi is used specifically for joiValidateAmount to provide a concise, robust
10
+ * schema-based validation, while preserving the original error messages and behavior.
11
+ * - The rest of the file keeps the lightweight, dependency-free constraint handling
12
+ * for other small validators, but joiValidateAmount now relies on Joi.
13
+ *
14
+ * Exported functions:
15
+ * - validateAlphanumeric(input)
16
+ * - validateLength(input, maxLength)
17
+ * - validateStatementDescription(input, maxLength = 22)
18
+ * - joiValidateAmount(amount)
19
+ * - joiValidate(data, constraints)
20
+ * - validateMetadataItemCount(metadata)
21
+ * - validateMetadataField(fieldName, fieldValue)
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const Joi = require("joi");
27
+
28
+ const Validator = {
29
+ /**
30
+ * Validate that the input has only alphanumeric characters and spaces
31
+ *
32
+ * @param {string} input
33
+ * @returns {string} input if valid
34
+ * @throws {Error} with suggested correction when invalid characters are present
35
+ */
36
+ validateAlphanumeric(input) {
37
+ if (typeof input !== "string") {
38
+ throw new Error("Input must be a string.");
39
+ }
40
+
41
+ const invalidCharPattern = /[^a-zA-Z0-9 ]/;
42
+ if (invalidCharPattern.test(input)) {
43
+ const suggestedInput = input.replace(/[^a-zA-Z0-9 ]/g, "");
44
+ throw new Error(
45
+ `The statement description contains invalid characters. Only alphanumeric characters and spaces are allowed. Suggested correction: '${suggestedInput}'`
46
+ );
47
+ }
48
+
49
+ return input;
50
+ },
51
+
52
+ /**
53
+ * Validate that the length of the input does not exceed the specified max length
54
+ *
55
+ * @param {string} input
56
+ * @param {number} maxLength
57
+ * @returns {string} input if valid
58
+ * @throws {Error} with suggested truncation when too long
59
+ */
60
+ validateLength(input, maxLength) {
61
+ if (typeof input !== "string") {
62
+ throw new Error("Input must be a string.");
63
+ }
64
+ if (typeof maxLength !== "number" || maxLength < 0) {
65
+ throw new Error("maxLength must be a non-negative number.");
66
+ }
67
+
68
+ if (input.length > maxLength) {
69
+ const suggestedInput = input.slice(0, maxLength);
70
+ throw new Error(
71
+ `The statement description exceeds the allowed length of ${maxLength} characters. Suggested correction: '${suggestedInput}'`
72
+ );
73
+ }
74
+
75
+ return input;
76
+ },
77
+
78
+ /**
79
+ * Full validation function: length + alphanumeric characters
80
+ *
81
+ * @param {string} input
82
+ * @param {number} [maxLength=22]
83
+ * @returns {string} input if validation passes
84
+ * @throws {Error}
85
+ */
86
+ validateStatementDescription(input, maxLength = 22) {
87
+ // Step 1: Ensure input has only alphanumeric characters and spaces
88
+ this.validateAlphanumeric(input);
89
+
90
+ // Step 2: Ensure length doesn't exceed the limit
91
+ this.validateLength(input, maxLength);
92
+
93
+ return input; // If validation passes, return the input
94
+ },
95
+
96
+ /**
97
+ * Combined regex and logical validation for amount, updated to use Joi.
98
+ *
99
+ * Mirrors the PHP behavior:
100
+ * - First checks regex: /^([0]|([1-9][0-9]{0,17}))([.][0-9]{0,2})?$/
101
+ * (up to 18 digits before decimal, up to 2 decimal places)
102
+ * - Then enforces "NotBlank" and "Positive" semantics: not blank, > 0
103
+ *
104
+ * Implementation notes:
105
+ * - Joi is used to validate the string pattern and presence.
106
+ * - Additional numeric positivity check is performed after Joi validation
107
+ * to ensure the behavior matches Symfony's Positive (which disallows zero).
108
+ *
109
+ * @param {string|number} amount
110
+ * @returns {string} the original amount string (trimmed)
111
+ * @throws {Error} with descriptive message when invalid
112
+ */
113
+ joiValidateAmount(amount) {
114
+ // Normalize input, preserve trimmed string for messages
115
+ const amountStr =
116
+ amount === null || amount === undefined ? "" : String(amount).trim();
117
+
118
+ // pawaPay's pattern: up to 18 digits before decimal, optional decimal with up to 2 places
119
+ const pattern = /^([0]|([1-9][0-9]{0,17}))([.][0-9]{0,2})?$/;
120
+
121
+ // Joi schema validates presence and regex pattern
122
+ const schema = Joi.string()
123
+ .required()
124
+ .pattern(pattern)
125
+ .messages({
126
+ "string.pattern.base": `The amount '${amountStr}' is invalid. The amount must be a number with up to 18 digits before the decimal point and up to 2 decimal places.`,
127
+ "any.required": "This value should not be blank.",
128
+ "string.empty": "This value should not be blank."
129
+ });
130
+
131
+ const { error } = schema.validate(amountStr);
132
+
133
+ if (error) {
134
+ // Joi message already tailored above, but ensure consistent message format
135
+ // If Joi provides a message, use it; otherwise fallback
136
+ throw new Error(error.message || `The amount '${amountStr}' is invalid.`);
137
+ }
138
+
139
+ // Convert to number and enforce Positive (Symfony's Positive disallows zero)
140
+ const numeric = Number(amountStr);
141
+ if (!Number.isFinite(numeric)) {
142
+ throw new Error(`The amount '${amountStr}' is not a valid number.`);
143
+ }
144
+ if (numeric <= 0) {
145
+ // Keep Symfony-like message
146
+ throw new Error("This value should be positive.");
147
+ }
148
+
149
+ return amountStr;
150
+ },
151
+
152
+ /**
153
+ * General lightweight validator to mimic Symfony behavior for simple constraints.
154
+ *
155
+ * Constraint descriptor examples:
156
+ * { type: 'NotBlank', message: '...' }
157
+ * { type: 'Length', max: 50, maxMessage: '...' }
158
+ * { type: 'Regex', pattern: /^[a-z]+$/, message: '...' }
159
+ * { type: 'Positive', message: '...' }
160
+ *
161
+ * The function throws the first encountered violation as an Error.
162
+ *
163
+ * Note: This function intentionally remains dependency-free and simple.
164
+ * For richer schema-based validation across many fields, consider building
165
+ * Joi schemas for each DTO and using Joi.validate instead.
166
+ *
167
+ * @param {any} data
168
+ * @param {Array<Object>} constraints
169
+ * @returns {any} data when valid
170
+ * @throws {Error} first violation message
171
+ */
172
+ joiValidate(data, constraints) {
173
+ for (let i = 0; i < constraints.length; i++) {
174
+ const c = constraints[i];
175
+ if (!c || typeof c.type !== "string") {
176
+ continue; // skip unknown descriptors
177
+ }
178
+
179
+ switch (c.type) {
180
+ case "NotBlank":
181
+ if (
182
+ data === null ||
183
+ data === undefined ||
184
+ (typeof data === "string" && data.trim() === "")
185
+ ) {
186
+ throw new Error(c.message || "This value should not be blank.");
187
+ }
188
+ break;
189
+
190
+ case "Positive":
191
+ {
192
+ const numeric = Number(data);
193
+ if (!Number.isFinite(numeric) || numeric <= 0) {
194
+ throw new Error(c.message || "This value should be positive.");
195
+ }
196
+ }
197
+ break;
198
+
199
+ case "Length":
200
+ if (typeof data === "string") {
201
+ if (typeof c.max === "number" && data.length > c.max) {
202
+ // use maxMessage if provided, otherwise a default that uses {{ limit }}
203
+ const msg = c.maxMessage
204
+ ? c.maxMessage.replace("{{ limit }}", String(c.max))
205
+ : `This value is too long. It should have ${c.max} characters or less.`;
206
+ throw new Error(msg);
207
+ }
208
+ if (typeof c.min === "number" && data.length < c.min) {
209
+ const msg = c.minMessage
210
+ ? c.minMessage.replace("{{ limit }}", String(c.min))
211
+ : `This value is too short. It should have ${c.min} characters or more.`;
212
+ throw new Error(msg);
213
+ }
214
+ } else {
215
+ // If not a string, skip length checks
216
+ }
217
+ break;
218
+
219
+ case "Regex":
220
+ {
221
+ if (typeof c.pattern === "undefined") {
222
+ break;
223
+ }
224
+ let regex = c.pattern;
225
+ // allow passing pattern as string or RegExp
226
+ if (typeof regex === "string") {
227
+ regex = new RegExp(regex);
228
+ }
229
+ if (typeof data !== "string" || !regex.test(data)) {
230
+ throw new Error(c.message || "This value is not valid.");
231
+ }
232
+ }
233
+ break;
234
+
235
+ default:
236
+ // unknown constraint, skip
237
+ break;
238
+ }
239
+ }
240
+
241
+ return data;
242
+ },
243
+
244
+ /**
245
+ * Validate that the number of metadata items does not exceed 10
246
+ *
247
+ * @param {Array} metadata
248
+ * @returns {Array} metadata if valid
249
+ * @throws {Error} when count > 10
250
+ */
251
+ validateMetadataItemCount(metadata) {
252
+ if (!Array.isArray(metadata)) {
253
+ throw new Error("Metadata must be an array.");
254
+ }
255
+ if (metadata.length > 10) {
256
+ throw new Error(
257
+ `Number of metadata items must not be more than 10. You provided ${metadata.length} items.`
258
+ );
259
+ }
260
+ return metadata;
261
+ },
262
+
263
+ /**
264
+ * Validate individual metadata fields: fieldName and fieldValue
265
+ *
266
+ * @param {string} fieldName
267
+ * @param {string} fieldValue
268
+ * @returns {{fieldName: string, fieldValue: string}} validated pair
269
+ * @throws {Error} if validation fails
270
+ */
271
+ validateMetadataField(fieldName, fieldValue) {
272
+ // Define constraints for fieldName
273
+ const fieldNameConstraints = [
274
+ { type: "NotBlank", message: "Metadata field name cannot be blank." },
275
+ {
276
+ type: "Length",
277
+ max: 50,
278
+ maxMessage: "Metadata field name cannot exceed {{ limit }} characters."
279
+ },
280
+ {
281
+ type: "Regex",
282
+ pattern: /^[a-zA-Z0-9_ ]+$/,
283
+ message:
284
+ "Metadata field name can only contain alphanumeric characters, underscores, and spaces."
285
+ }
286
+ ];
287
+
288
+ // Define constraints for fieldValue
289
+ const fieldValueConstraints = [
290
+ { type: "NotBlank", message: "Metadata field value cannot be blank." },
291
+ {
292
+ type: "Length",
293
+ max: 100,
294
+ maxMessage: "Metadata field value cannot exceed {{ limit }} characters."
295
+ },
296
+ {
297
+ type: "Regex",
298
+ pattern: /^[a-zA-Z0-9_\-., ]+$/,
299
+ message:
300
+ "Metadata field value can only contain alphanumeric characters, underscores, hyphens, periods, commas, and spaces."
301
+ }
302
+ ];
303
+
304
+ // Validate fieldName
305
+ this.joiValidate(fieldName, fieldNameConstraints);
306
+
307
+ // Validate fieldValue
308
+ this.joiValidate(fieldValue, fieldValueConstraints);
309
+
310
+ return {
311
+ fieldName,
312
+ fieldValue
313
+ };
314
+ }
315
+ };
316
+
317
+ module.exports = Validator;