luma-sdk
v2.0.2
Published
A simple iframe postMessage wrapper with request/response support using UUIDs.
Maintainers
Readme
Faranex IframeClient
برقراری ارتباط بین اپلیکیشن شما و پنل لوما
ویژگیها
✨ ویژگیهای کلیدی:
- 🔒 پیادهسازی TypeScript با پشتیبانی کامل از نوعها
- 📨 ارتباط دوطرفه با iframe
- 🔑 ردیابی درخواست/پاسخ بر اساس UUID
- ⌨️ مدیریت خودکار رویدادهای کیبورد
- ⏱️ مدیریت timeout قابل تنظیم
- 🎯 API ساده و قابل فهم
- 📦 بدون وابستگی خارجی (به جز UUID)
نصب
npm install luma-sdkیا با استفاده از pnpm:
pnpm add luma-sdkیا با استفاده از yarn:
yarn add luma-sdkشروع سریع
استفاده پایه
import { IframeClient } from "luma-sdk";
// ایجاد یک نمونه از کلاینت
const client = new IframeClient();
// ارسال پیام و انتظار برای پاسخ
try {
const response = await client.sendMessage({
type: "AUTH_TOKEN",
data: "",
});
console.log("پاسخ دریافت شد:", response);
} catch (error) {
console.error("خطا:", error);
}راهاندازی
ایجاد نمونه IframeClient
برای استفاده از کتابخانه، ابتدا باید یک نمونه از کلاس IframeClient ایجاد کنید:
import { IframeClient } from "luma-sdk";
const iframeElement = document.getElementById("myIframe") as HTMLIFrameElement;
const client = new IframeClient(
window.parent, // پنجره iframe
"https://trusted-domain.com", // origin مورد اعتماد (اختیاری)
50000 // timeout به میلیثانیه (اختیاری)
);پارامترهای سازنده
new IframeClient(
iframeWindow: Window,
targetOrigin?: string,
timeout?: number
)پارامترها:
iframeWindow(الزامی) - پنجره iframe که میخواهید با آن ارتباط برقرار کنید. معمولاًiframeElement.contentWindowیاwindow.parentاست.targetOrigin(اختیاری، پیشفرض:"https://kiosk.luma360.ir") - origin مورد اعتماد برای امنیت. در production حتماً یک origin مشخص کنید و از"*"استفاده نکنید.timeout(اختیاری، پیشفرض:50000) - زمان انتظار برای پاسخ به میلیثانیه. باید بین 40000 تا 60000 باشد.
مثال:
const client = new IframeClient();
const client = new IframeClient(window.parent, "https://my-trusted-domain.com");
const client = new IframeClient(
window.parent,
"https://my-trusted-domain.com",
55000
);انواع پیامها
کتابخانه از انواع پیامهای زیر پشتیبانی میکند:
type MessageType =
| "AUTH_TOKEN" // دریافت توکن احراز هویت
| "PRINT_DATA" // ارسال داده برای چاپ
| "PAYMENT" // ارسال اطلاعات پرداخت
| "KEYBOARD_EVENT"; // رویدادهای کیبورد1. AUTH_TOKEN
برای دریافت توکن احراز هویت از اپلیکیشن والد:
try {
const response = await client.sendMessage({
type: "AUTH_TOKEN",
data: "", // null or undefined
});
console.log("توکن دریافت شد:", response.token);
} catch (error) {
console.error("خطا در دریافت توکن:", error);
}2. PRINT_DATA
برای ارسال دادهها به منظور چاپ:
try {
const printData = {
content: "متن یا تصویر مورد نظر برای چاپ مورد نظر برای چاپ", // base64 image
};
const response = await client.sendMessage({
type: "PRINT_DATA",
data: encrypt(printData),
});
console.log("نتیجه چاپ:", response);
} catch (error) {
console.error("خطا در چاپ:", error);
}3. PAYMENT
برای ارسال اطلاعات پرداخت:
try {
const paymentData = {
amount: 100000,
};
const response = await client.sendMessage({
type: "PAYMENT",
data: encrypt(paymentData),
});
console.log("نتیجه پرداخت:", response);
} catch (error) {
console.error("خطا در پرداخت:", error);
}4. KEYBOARD_EVENT
برای مدیریت رویدادهای کیبورد. این نوع پیام به صورت خودکار توسط KeyboardManager مدیریت میشود و معمولاً نیازی به ارسال دستی ندارد.
مدیریت کیبورد
کتابخانه به صورت خودکار رویدادهای کیبورد را مدیریت میکند. وقتی کاربر روی یک input یا textarea در iframe فوکوس میکند، کتابخانه به صورت خودکار:
- رویداد
FOCUSEDرا ارسال میکند - input را به حالت
readonlyتبدیل میکند تا کیبورد مجازی نمایش داده شود - رویدادهای کیبورد را از اپلیکیشن والد دریافت و پردازش میکند
رویدادهای کیبورد
کتابخانه از رویدادهای زیر پشتیبانی میکند:
- insert_text: وارد کردن متن در موقعیت cursor
- backspace: حذف کاراکتر قبل از cursor
- enter: ارسال فرم (submit)
این رویدادها به صورت خودکار مدیریت میشوند و نیازی به کدنویسی اضافی ندارید.
API مرجع
IframeClient
متدها
sendMessage(data: RequestData): Promise<any>
ارسال پیام به iframe و انتظار برای پاسخ.
پارامترها:
data- داده درخواست شاملtypeوdata
برمیگرداند: Promise که با داده پاسخ resolve میشود یا در صورت خطا/timeout reject میشود
مثال:
try {
const result = await client.sendMessage({
type: "PAYMENT",
data: encrypt({ amount: 100 }),
});
console.log("پرداخت موفق:", result);
} catch (error) {
if (error.message === "Response timed out") {
console.error("درخواست timeout شد");
} else {
console.error("خطا:", error);
}
}getTargetOrigin(): string
دریافت origin هدف تنظیم شده.
getTimeout(): number
دریافت مقدار timeout تنظیم شده.
مدیریت خطاها
خطاهای رایج
1. Timeout
اگر درخواست در زمان مشخص شده پاسخ نگیرد:
try {
const response = await client.sendMessage({
type: "PAYMENT",
data: "payment-data",
});
} catch (error) {
if (error.message === "Response timed out") {
console.error("درخواست timeout شد. لطفاً دوباره تلاش کنید.");
}
}2. خطای اعتبارسنجی Timeout
اگر timeout خارج از محدوده مجاز تنظیم شود:
try {
const client = new IframeClient(window.parent, "*", 30000); // ❌ خطا
} catch (error) {
if (error.message.includes("Timeout must be")) {
console.error("مقدار timeout باید بین 40000 تا 60000 میلیثانیه باشد");
}
}3. خطای Origin
اگر origin پیام با origin تنظیم شده مطابقت نداشته باشد، پیام نادیده گرفته میشود.
بهترین روشهای مدیریت خطا
async function communicateWithIframe() {
try {
const response = await client.sendMessage({
type: "PRINT_DATA",
data: "document content",
});
console.log("موفق:", response);
} catch (error) {
if (error.message === "Response timed out") {
console.error("ifram در زمان مشخص شده پاسخ نداد");
// میتوانید منطق retry را اینجا پیادهسازی کنید
} else if (error.message.startsWith("Timeout must be")) {
console.error("پیکربندی timeout نامعتبر است");
} else {
console.error("خطای غیرمنتظره:", error.message);
}
}
}امنیت
استفاده از Target Origin
برای امنیت بیشتر، همیشه یک origin مورد اعتماد مشخص کنید:
// ❌ بد - استفاده از '*' در production
const client = new IframeClient(window.parent, "*");
// ✅ خوب - استفاده از origin مشخص
const client = new IframeClient(
window.parent,
"https://trusted-iframe.example.com"
);اعتبارسنجی دادهها
قبل از ارسال دادهها، حتماً آنها را اعتبارسنجی کنید:
function sendPayment(amount: number, currency: string) {
// اعتبارسنجی دادهها
if (amount <= 0) {
throw new Error("مقدار پرداخت باید بیشتر از صفر باشد");
}
return client.sendMessage({
type: "PAYMENT",
data: encrypt({ amount }),
});
}فرایند Encryption
برای امنیت بیشتر در ارتباطات، میتوانید از encryption استفاده کنید. کتابخانه از الگوریتم AES-256-CBC پشتیبانی میکند.
الگوریتم Encryption
- الگوریتم: AES-256-CBC
- Key Derivation: SHA-256 از key string
- IV: 16 بایت (pad یا truncate میشود)
- Padding: PKCS7
- Encoding: Base64
مثالهای پیادهسازی
TypeScript/JavaScript
import forge from "node-forge";
export function deriveKeyAndIv(keyStr: string, ivStr: string) {
const key = forge.md.sha256.create();
key.update(keyStr);
const keyBytes = key.digest().getBytes(); // 32 bytes
const ivUtf8 = forge.util.encodeUtf8(ivStr);
const ivPadded = (ivUtf8 + "\0".repeat(16)).slice(0, 16);
return { key: keyBytes, iv: ivPadded };
}
export function encryptAes256Cbc(
plainText: string,
keyStr: string,
ivStr: string
) {
const { key, iv } = deriveKeyAndIv(keyStr, ivStr);
const cipher = forge.cipher.createCipher("AES-CBC", key);
cipher.start({ iv });
cipher.update(forge.util.createBuffer(plainText, "utf8"));
cipher.finish();
return forge.util.encode64(cipher.output.getBytes());
}
export function decryptAes256Cbc(
cipherBase64: string,
keyStr: string,
ivStr: string
) {
const { key, iv } = deriveKeyAndIv(keyStr, ivStr);
const decipher = forge.cipher.createDecipher("AES-CBC", key);
decipher.start({ iv });
decipher.update(forge.util.createBuffer(forge.util.decode64(cipherBase64)));
decipher.finish();
return decipher.output.toString("utf8");
}
// استفاده
const encrypted = encryptAes256Cbc(
"متن مورد نظر",
"my-secret-key",
"my-iv-string"
);
const decrypted = decryptAes256Cbc(encrypted, "my-secret-key", "my-iv-string");Python
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def derive_key_and_iv(key_str, iv_str):
key = hashlib.sha256(key_str.encode("utf-8")).digest() # 32 bytes
iv = iv_str.encode("utf-8")[:16].ljust(16, b"\0") # pad or truncate
return key, iv
def encrypt_aes256_cbc(plain_text, key_str, iv_str):
key, iv = derive_key_and_iv(key_str, iv_str)
cipher = AES.new(key, AES.MODE_CBC, iv)
padded = pad(plain_text.encode("utf-8"), AES.block_size)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode("utf-8")
def decrypt_aes256_cbc(cipher_base64, key_str, iv_str):
key, iv = derive_key_and_iv(key_str, iv_str)
cipher_data = base64.b64decode(cipher_base64)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(cipher.decrypt(cipher_data), AES.block_size)
return decrypted.decode("utf-8")
# استفاده
encrypted = encrypt_aes256_cbc("متن مورد نظر", "my-secret-key", "my-iv-string")
decrypted = decrypt_aes256_cbc(encrypted, "my-secret-key", "my-iv-string")PHP
function deriveKeyAndIv($keyStr, $ivStr) {
$key = hash('sha256', $keyStr, true); // 32 bytes binary
$iv = substr(str_pad($ivStr, 16, "\0"), 0, 16); // pad or truncate
return [$key, $iv];
}
function encryptAes256Cbc($plainText, $keyStr, $ivStr) {
list($key, $iv) = deriveKeyAndIv($keyStr, $ivStr);
$encrypted = openssl_encrypt($plainText, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted);
}
function decryptAes256Cbc($cipherBase64, $keyStr, $ivStr) {
list($key, $iv) = deriveKeyAndIv($keyStr, $ivStr);
$cipher = base64_decode($cipherBase64);
return openssl_decrypt($cipher, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
}
// استفاده
$encrypted = encryptAes256Cbc("متن مورد نظر", "my-secret-key", "my-iv-string");
$decrypted = decryptAes256Cbc($encrypted, "my-secret-key", "my-iv-string");Java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
public class AESCrypto {
public static byte[] deriveKey(String keyStr) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(keyStr.getBytes("UTF-8"));
}
public static byte[] deriveIv(String ivStr) throws Exception {
byte[] iv = new byte[16];
byte[] bytes = ivStr.getBytes("UTF-8");
System.arraycopy(bytes, 0, iv, 0, Math.min(bytes.length, 16));
return iv;
}
public static String encryptAes256Cbc(String plainText, String keyStr, String ivStr) throws Exception {
byte[] key = deriveKey(keyStr);
byte[] iv = deriveIv(ivStr);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
public static String decryptAes256Cbc(String cipherBase64, String keyStr, String ivStr) throws Exception {
byte[] key = deriveKey(keyStr);
byte[] iv = deriveIv(ivStr);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
return new String(decrypted, "UTF-8");
}
}
// استفاده
String encrypted = AESCrypto.encryptAes256Cbc("متن مورد نظر", "my-secret-key", "my-iv-string");
String decrypted = AESCrypto.decryptAes256Cbc(encrypted, "my-secret-key", "my-iv-string");Go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
)
func deriveKeyAndIv(keyStr, ivStr string) ([]byte, []byte) {
key := sha256.Sum256([]byte(keyStr)) // 32 bytes
iv := make([]byte, 16)
copy(iv, []byte(ivStr)) // pad or truncate
return key[:], iv
}
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
func pkcs7Unpad(data []byte) []byte {
length := len(data)
unpadding := int(data[length-1])
return data[:(length - unpadding)]
}
func encryptAes256Cbc(plainText, keyStr, ivStr string) (string, error) {
key, iv := deriveKeyAndIv(keyStr, ivStr)
block, _ := aes.NewCipher(key)
padded := pkcs7Pad([]byte(plainText), aes.BlockSize)
cipherText := make([]byte, len(padded))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText, padded)
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func decryptAes256Cbc(cipherBase64, keyStr, ivStr string) (string, error) {
key, iv := deriveKeyAndIv(keyStr, ivStr)
block, _ := aes.NewCipher(key)
cipherData, _ := base64.StdEncoding.DecodeString(cipherBase64)
decrypted := make([]byte, len(cipherData))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decrypted, cipherData)
unpadded := pkcs7Unpad(decrypted)
return string(unpadded), nil
}
// استفاده
encrypted, _ := encryptAes256Cbc("متن مورد نظر", "my-secret-key", "my-iv-string")
decrypted, _ := decryptAes256Cbc(encrypted, "my-secret-key", "my-iv-string")C#
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class AESCrypto
{
private static void DeriveKeyAndIv(string keyStr, string ivStr, out byte[] key, out byte[] iv)
{
using (SHA256 sha = SHA256.Create())
{
key = sha.ComputeHash(Encoding.UTF8.GetBytes(keyStr));
}
iv = new byte[16];
byte[] ivBytes = Encoding.UTF8.GetBytes(ivStr);
Array.Copy(ivBytes, iv, Math.Min(ivBytes.Length, 16));
}
public static string EncryptAes256Cbc(string plainText, string keyStr, string ivStr)
{
DeriveKeyAndIv(keyStr, ivStr, out byte[] key, out byte[] iv);
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var encryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(cipherBytes);
}
}
public static string DecryptAes256Cbc(string cipherBase64, string keyStr, string ivStr)
{
DeriveKeyAndIv(keyStr, ivStr, out byte[] key, out byte[] iv);
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var decryptor = aes.CreateDecryptor();
byte[] cipherBytes = Convert.FromBase64String(cipherBase64);
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(plainBytes);
}
}
}
// استفاده
string encrypted = AESCrypto.EncryptAes256Cbc("متن مورد نظر", "my-secret-key", "my-iv-string");
string decrypted = AESCrypto.DecryptAes256Cbc(encrypted, "my-secret-key", "my-iv-string");استفاده از Encryption در ارتباطات
برای استفاده از encryption در ارتباطات با iframe:
import { IframeClient } from "luma-sdk";
import { encryptAes256Cbc, decryptAes256Cbc } from "./encryption";
const client = new IframeClient(iframeWindow);
// ارسال داده encrypted
async function sendEncryptedData(data: any) {
const plainText = JSON.stringify(data);
const encrypted = encryptAes256Cbc(
plainText,
"my-secret-key",
"my-iv-string"
);
const response = await client.sendMessage({
type: "PAYMENT",
data: encrypted,
});
// اگر پاسخ هم encrypted است
const decryptedResponse = decryptAes256Cbc(
response.data,
"my-secret-key",
"my-iv-string"
);
return JSON.parse(decryptedResponse);
}مثالهای عملی
مثال 1: دریافت توکن و استفاده از آن
import { IframeClient } from "luma-sdk";
const iframeElement = document.getElementById("myIframe") as HTMLIFrameElement;
const client = new IframeClient(iframeElement.contentWindow);
async function initializeApp() {
try {
// دریافت توکن
const authResponse = await client.sendMessage({
type: "AUTH_TOKEN",
data: "",
});
const token = authResponse.token;
console.log("توکن دریافت شد:", token);
// استفاده از توکن برای درخواستهای بعدی
// ...
} catch (error) {
console.error("خطا در دریافت توکن:", error);
}
}مثال 2: پرداخت
async function processPayment(amount: number, retries = 3) {
const paymentData = {
amount,
};
try {
const response = await client.sendMessage({
type: "PAYMENT",
data: encrypt(paymentData),
});
if (response.success) {
return response;
}
throw new Error(response.message || "پرداخت ناموفق بود");
} catch (error) {
if (error.message === "Response timed out" && i < retries - 1) {
console.log(`تلاش ${i + 1} ناموفق بود. تلاش مجدد...`);
await new Promise((resolve) => setTimeout(resolve, 1000)); // انتظار 1 ثانیه
continue;
}
throw error;
}
throw new Error("پرداخت پس از چندین تلاش ناموفق بود");
}مثال 3: استفاده در React Component
import { useEffect, useRef } from "react";
import { IframeClient } from "luma-sdk";
function MyIframeComponent() {
const clientRef = useRef<IframeClient | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
if (iframeRef.current) {
clientRef.current = new IframeClient();
}
return () => {
// Cleanup اگر نیاز باشد
};
}, []);
const handlePayment = async () => {
if (!clientRef.current) return;
try {
const response = await clientRef.current.sendMessage({
type: "PAYMENT",
data: encrypt({ amount: 100000 }),
});
console.log("پرداخت موفق:", response);
} catch (error) {
console.error("خطا در پرداخت:", error);
}
};
return (
<div>
<button onClick={handlePayment}>پرداخت</button>
</div>
);
}تنظیمات Timeout
مقدار timeout باید بین 40000 تا 60000 میلیثانیه باشد:
// ✅ معتبر
new IframeClient(window, "*", 40000); // 40 ثانیه
new IframeClient(window, "*", 50000); // 50 ثانیه (پیشفرض)
new IframeClient(window, "*", 60000); // 60 ثانیه
// ❌ نامعتبر - خطا میدهد
new IframeClient(window, "*", 30000); // خیلی کم
new IframeClient(window, "*", 70000); // خیلی زیادبهترین روشها
- همیشه origin مشخص کنید - در production از
"*"استفاده نکنید - خطاها را مدیریت کنید - منطق retry برای timeout پیادهسازی کنید
- دادهها را اعتبارسنجی کنید - قبل از ارسال، دادهها را بررسی کنید
- از TypeScript استفاده کنید - از مزایای type safety بهره ببرید
- پیامها را کوچک نگه دارید - از ارسال payloadهای بزرگ خودداری کنید
- از encryption استفاده کنید - برای دادههای حساس از encryption استفاده کنید
- Cleanup کنید - در صورت نیاز، event listenerها را پاک کنید
پشتیبانی از مرورگرها
- Chrome/Edge 90+
- Firefox 88+
- Safari 15+
- هر مرورگر مدرنی که از API
postMessageپشتیبانی کند
ساختار پروژه
src/
├── index.ts # نقطه ورود اصلی
├── types.ts # تعاریف نوع
├── iframe-client.ts # کلاس IframeClient
└── keyboard-manager.ts # کلاس KeyboardManager
dist/
├── index.js # JavaScript کامپایل شده
├── index.d.ts # تعاریف TypeScriptType Definitions
همه typeها از کتابخانه export میشوند:
import {
IframeClient,
KeyboardManager,
RequestData,
ResponseMessage,
MessageType,
FocusableElement,
PendingRequest,
KeyboardEventData,
} from "luma-sdk";RequestData
type RequestData = {
uuid?: string;
type: MessageType;
data: any;
};ResponseMessage
interface ResponseMessage {
type: MessageType | "ERROR" | "KEYBOARD_ON" | "KEYBOARD_OFF";
uuid: string;
data?: any;
error?: string;
}مجوز
MIT
نویسنده
Ali Abolfathi From Faranex
