@muhammedaksam/sipay-node
Version:
Node.js TypeScript SDK for Sipay payment gateway
332 lines • 12.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.luhnCheck = luhnCheck;
exports.validateCreditCard = validateCreditCard;
exports.maskCardNumber = maskCardNumber;
exports.formatAmount = formatAmount;
exports.formatAmountForHash = formatAmountForHash;
exports.generatePaymentHashKey = generatePaymentHashKey;
exports.generateStatusHashKey = generateStatusHashKey;
exports.generateConfirmPaymentHashKey = generateConfirmPaymentHashKey;
exports.generateHashKey = generateHashKey;
exports.generateInvoiceId = generateInvoiceId;
exports.validatePaymentData = validatePaymentData;
exports.maskCreditCard = maskCreditCard;
exports.parseSipayError = parseSipayError;
exports.validateHashKey = validateHashKey;
exports.generateServerFormatHashKey = generateServerFormatHashKey;
const crypto = __importStar(require("crypto"));
const { createHash, createCipheriv, createDecipheriv } = crypto;
/**
* Utility functions for Sipay SDK
*/
/**
* Validate credit card number using Luhn algorithm
*/
function luhnCheck(cardNumber) {
const digits = cardNumber.replace(/\D/g, '').split('').map(Number);
// Check minimum length
if (digits.length < 13 || digits.length > 19) {
return false;
}
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = digits[i];
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
/**
* Validate credit card number (alias for luhnCheck)
*/
function validateCreditCard(cardNumber) {
return luhnCheck(cardNumber);
}
/**
* Mask credit card number for display
*/
function maskCardNumber(cardNumber) {
const cleaned = cardNumber.replace(/\D/g, '');
if (cleaned.length < 8)
return cleaned;
const first4 = cleaned.slice(0, 4);
const last4 = cleaned.slice(-4);
const middle = '*'.repeat(cleaned.length - 8);
return `${first4}${middle}${last4}`;
}
/**
* Format amount for Sipay API - amounts should be sent as numbers, not cents
*/
function formatAmount(amount) {
// Amount should be formatted as a float with proper decimal places
// For amounts like 100.00, it should be "100.00", not "1.00"
return amount.toFixed(2);
}
/**
* Format amount specifically for hash key generation
* This should match PHP's number_format($amount, 2, ".", "") exactly
* PHP always formats with 2 decimal places: 10.00, 125.50, etc.
*/
function formatAmountForHash(amount) {
// Match PHP number_format($amount, 2, ".", "") - always 2 decimal places
return amount.toFixed(2);
}
/**
* Generate hash key for payment requests
* Order matches PHP SDK CreateNonSecurePaymentRequest::generateHashKeyParts():
* 1. total, 2. installments_number, 3. currency_code, 4. merchant_key, 5. invoice_id
*/
function generatePaymentHashKey(total, installment, currencyCode, merchantKey, invoiceId, apiSecret) {
const parts = [
formatAmountForHash(total),
installment.toString(),
currencyCode,
merchantKey,
invoiceId,
];
return generateHashKey(parts, apiSecret);
}
/**
* Generate hash key for status check requests
* Order matches PHP SDK CheckTransactionStatusRequest::generateHashKeyParts():
* 1. invoice_id, 2. merchant_key
*/
function generateStatusHashKey(invoiceId, merchantKey, apiSecret) {
const parts = [invoiceId, merchantKey];
return generateHashKey(parts, apiSecret);
}
/**
* Generate hash key for payment confirmation requests
* Order matches PHP SDK: merchant_key|invoice_id|status
*/
function generateConfirmPaymentHashKey(merchantKey, invoiceId, status, apiSecret) {
const parts = [merchantKey, invoiceId, status.toString()];
return generateHashKey(parts, apiSecret);
}
/**
* Generate hash key for payment requests
* Exact 1:1 Node.js implementation of PHP's generateHashKey function
* Matches: $total . '|' . $installment . '|' . $currency_code . '|' . $merchant_key . '|' . $invoice_id
* with openssl_encrypt('aes-256-cbc') encryption
*/
function generateHashKey(parts, appSecret) {
const data = parts.join('|');
// Generate IV using SHA1 hash of random value
const iv = createHash('sha1').update(String(Math.random())).digest('hex').slice(0, 16);
// Generate password using SHA1 hash of app secret
const password = createHash('sha1').update(appSecret).digest('hex');
// Generate salt using SHA1 hash of random value
const salt = createHash('sha1').update(String(Math.random())).digest('hex').slice(0, 4);
// Create encryption key using SHA256 hash of password and salt
const saltWithPassword = createHash('sha256')
.update(password + salt)
.digest('hex')
.slice(0, 32);
// Encrypt data using AES-256-CBC
const cipher = createCipheriv('aes-256-cbc', saltWithPassword, iv);
let encrypted = cipher.update(data, 'binary', 'base64');
encrypted += cipher.final('base64');
// Bundle components and replace forward slashes with double underscores
let msgEncryptedBundle = `${iv}:${salt}:${encrypted}`;
msgEncryptedBundle = msgEncryptedBundle.replace(/\//g, '__');
return msgEncryptedBundle;
}
/**
* Generate a unique invoice ID
*/
function generateInvoiceId(prefix = 'INV') {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 10000);
return `${prefix}${timestamp}${random}`;
}
/**
* Validate required payment fields
*/
function validatePaymentData(data) {
const errors = [];
const requiredFields = [
'cc_holder_name',
'cc_no',
'expiry_month',
'expiry_year',
'currency_code',
'invoice_id',
'total',
'name',
'surname',
'items',
];
for (const field of requiredFields) {
if (!data[field]) {
errors.push(`Missing required field: ${field}`);
}
}
// Validate credit card
if (data.cc_no && !validateCreditCard(data.cc_no)) {
errors.push('Invalid credit card number');
}
// Validate amount
if (data.total && (isNaN(data.total) || data.total <= 0)) {
errors.push('Invalid amount');
}
// Validate currency
const validCurrencies = ['TRY', 'USD', 'EUR'];
if (data.currency_code && !validCurrencies.includes(data.currency_code)) {
errors.push('Invalid currency code');
}
// Validate expiry
if (data.expiry_month) {
const month = parseInt(data.expiry_month, 10);
if (isNaN(month) || month < 1 || month > 12) {
errors.push('Invalid expiry month');
}
}
if (data.expiry_year) {
const year = parseInt(data.expiry_year, 10);
const currentYear = new Date().getFullYear();
if (isNaN(year) || year < currentYear || year > currentYear + 20) {
errors.push('Invalid expiry year');
}
}
return errors;
}
/**
* Mask credit card number for logging
*/
function maskCreditCard(cardNumber) {
const num = cardNumber.replace(/\D/g, '');
if (num.length <= 3) {
return '*'.repeat(num.length);
}
if (num.length <= 7) {
const firstThree = num.substring(0, 3);
const middle = '*'.repeat(num.length - 4);
const lastOne = num.substring(num.length - 1);
return `${firstThree}${middle}${lastOne}`;
}
const firstFour = num.substring(0, 4);
const lastFour = num.substring(num.length - 4);
const middle = '*'.repeat(Math.max(0, num.length - 8));
return `${firstFour}${middle}${lastFour}`;
}
/**
* Parse Sipay error response
*/
function parseSipayError(error) {
if (error.response?.data) {
return {
code: error.response.data.status_code || 0,
message: error.response.data.status_description || error.response.data.message || 'Unknown error',
};
}
return {
code: 0,
message: error.message || 'Network error',
};
}
/**
* Validate hash key using server-side validation logic
* This matches exactly what the PHP SDK does for server-side validation
* Expected decrypted format: status|total|invoiceId|orderId|currencyCode
*/
function validateHashKey(hashKey, secretKey) {
let status = '';
let currencyCode = '';
let total = 0;
let invoiceId = '';
let orderId = 0;
if (!hashKey) {
return [status, total, invoiceId, orderId, currencyCode];
}
try {
// Replace underscores with forward slashes (PHP compatibility)
const normalizedHashKey = hashKey.replace(/_/g, '/');
const password = createHash('sha1').update(secretKey).digest('hex');
const components = normalizedHashKey.split(':');
if (components.length > 2) {
const iv = components[0] || '';
const saltHex = components[1] || '';
const encryptedMsg = components[2] || '';
// Generate salt exactly like PHP: hash('sha256', $password . $salt)
const saltWithPassword = createHash('sha256')
.update(password + saltHex)
.digest('hex');
// Create decipher with proper parameters
// PHP's openssl_decrypt expects:
// - IV as raw ASCII bytes (16-char hex string treated as ASCII)
// - Key as binary from hex string (32 bytes for AES-256)
const decipher = createDecipheriv('aes-256-cbc', Buffer.from(saltWithPassword, 'hex').slice(0, 32), // 32-byte key from hex
Buffer.from(iv, 'ascii') // IV: 16-char hex string as ASCII bytes
);
let decrypted = decipher.update(encryptedMsg, 'base64', 'utf8');
decrypted += decipher.final('utf8');
if (decrypted.includes('|')) {
const array = decrypted.split('|');
status = array[0] || '0';
total = parseFloat(array[1] || '0');
invoiceId = array[2] || '0';
orderId = parseInt(array[3] || '0');
currencyCode = array[4] || '';
}
}
}
catch {
// Silently handle decryption errors
}
return [status, total, invoiceId, orderId, currencyCode];
}
/**
* Generate hash key in server validation format
* Server expects: status|total|invoiceId|orderId|currencyCode
*/
function generateServerFormatHashKey(status, total, invoiceId, orderId, currencyCode, appSecret) {
const parts = [status, formatAmountForHash(total), invoiceId, orderId.toString(), currencyCode];
return generateHashKey(parts, appSecret);
}
// Export status code helpers
__exportStar(require("./status-code-helpers"), exports);
//# sourceMappingURL=index.js.map