@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +19 -0
- package/README.md +252 -0
- package/data/active_conf.json +739 -0
- package/data/active_conf_v1.json +740 -0
- package/data/active_conf_v2.json +2531 -0
- package/data/mno_availability.json +1117 -0
- package/data/mno_availability_v1.json +1479 -0
- package/data/mno_availability_v2.json +317 -0
- package/package.json +103 -0
- package/src/api/ApiClient.js +1 -0
- package/src/api/index.js +7 -0
- package/src/config/Config.js +137 -0
- package/src/config/index.js +7 -0
- package/src/index.js +18 -0
- package/src/utils/failureCodeHelper.js +237 -0
- package/src/utils/helpers.js +55 -0
- package/src/utils/license/index.js +16 -0
- package/src/utils/license/integrity.js +1 -0
- package/src/utils/license/protection.js +1 -0
- package/src/utils/license/server-check.js +1 -0
- package/src/utils/license/validator.js +81 -0
- package/src/utils/validator.js +317 -0
|
@@ -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;
|