UNPKG

@muhammedaksam/sipay-node

Version:

Node.js TypeScript SDK for Sipay payment gateway

332 lines 12.3 kB
"use strict"; 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