حزمة SDK لـ TypeScript و Node.js

@anis-ly/partners — لـ Node.js 22 وما بعده، مكتوبة بلغة TypeScript، مع إصدارين بصيغتي ES module وCommonJS وأنواع كاملة. تستخدم fetch وWebCrypto المدمجتين في Node؛ وتبعيتها الوحيدة هي واجهة OpenTelemetry API.

أداة مساعدة اختيارية، تُقدَّم كما هي

كل حزم SDK من أنيس مجانية وتُقدَّم بموجب رخصة MIT «كما هي»، دون أي ضمان من أي نوع. ولا تتحمّل أنيس (Aniscom for Technical Services) أي مسؤولية أو التزام عن استخدامها أو عن أي خسارة تنشأ عنه. وتبقى أنت المسؤول عن ربط نظامك — تسجيل طلبات الشراء قبل إرسالها، والاستئناف، وحفظ المفاتيح، والاختبار. الشيفرة المصدرية علنية: اقرأها لتفهم بدقة ما تفعله قبل أن تعتمد عليها. ولا تحتاج إلى حزمة SDK أصلًا — يمكنك استدعاء واجهة API لشركاء أنيس مباشرة باتباع دليل الاستخدام وصفحة مرجع API.

حزمة SDK بالإصدار 2.2.0. إصدارات 1.x مهجورة؛ راجع سجل التغييرات.

التثبيت

npm install @anis-ly/partners@2.2.0

يُنشر كل إصدار من GitHub مع بيان منشأ موقَّع، تعرضه npm في صفحة الحزمة.

الإعداد

import { AnisPartnersClient, PemP256Signer } from '@anis-ly/partners';

const signer = (await PemP256Signer.fromPemFile('/secure/partner-key.pem')).forKey(keyId);
const client = AnisPartnersClient.create({
  options: {
    authority: 'https://<the address Anis gave you>',
    signatureLifetimeSeconds: 60,
    acceptLanguage: 'ar',
    signingKeyCacheSeconds: 300,
    timeoutMs: 30_000,
  },
  signer,
});

تُفحص الإعدادات عند إنشاء العميل: فعنوان ليس HTTPS (يُقبل HTTP العادي على جهازك أنت فقط)، أو مدة صلاحية توقيع خارج النطاق من 1 إلى 60 ثانية، أو مدة حفظ للمفاتيح تتجاوز 5 دقائق، يوقفه عند تلك النقطة.

المُوقِّع خطوة منفصلة عن قصد. يوقّع PemP256Signer باستخدام ملف مفتاح. أما مع خزنة أسرار أو وحدة أمان عتادية، فنفّذ RequestSigner — معرّف مفتاحك keyId ودالة sign(bytes) تعيد التوقيع ذا الـ 64 بايت — ولا يدخل المفتاح إلى عمليتك أبدًا.

البيئات عديمة الخوادم والمضيفات متعددة النسخ: مرّر keyCache (بدالتي get وset) لكي تتشارك النسخ وثيقة مفاتيح التوقيع العامة الخاصة بأنيس بدل أن تجلبها كل نسخة بنفسها. استخدم ذاكرة مؤقتة لا يكتب فيها غير تطبيقك. تحتفظ الحزمة بقائمة مفاتيح أنيس لمدة لا تتجاوز 5 دقائق من وقت جلبها، حتى حين تأتي من الذاكرة المؤقتة المشتركة.

يقبل كل استدعاء { signal }، وهو AbortSignal يلغي كل ما يفعله الاستدعاء.

استخدمها

const profile = await client.profile.get();

for await (const wallet of client.wallets.list()) {                       // every page, followed for you
  console.log(wallet.name ?? wallet.id, wallet.balance.toString());
}

for await (const card of client.catalogue.listCards(walletId, subcategoryId)) {
  console.log(card.id, card.unitPrice?.toString());
}

const credential = await client.ownedCards.reveal(walletId, soldCardId);

المبالغ قيم من النوع Money بثلاث خانات عشرية بالضبط — وليست أعداد JavaScript أبدًا. تجري Money.of('10.500', 'LYD') وprice.multiply(2) حسابًا دقيقًا.

ما الذي تتحقق منه

منذ الإصدار 2.0.1، تتحقق الحزمة من كل ردّ على الاستدعاءات التي توقّع أنيس ردودها — تقديم طلب شراء، وقراءة طلب شراء، وكشف بطاقة، وكشف بطاقات فاتورة، واستدعاءات تسجيل المفتاح الأربعة، وتعليق التطبيق، وإلغاء المفتاح، والفحص الذاتي للتوقيع — نجاحًا كان الردّ أم رفضًا. وتشترط وجود التوقيع هناك: فالردّ على أحد هذه الاستدعاءات الذي لا يحمل توقيعًا صحيحًا يُتخلَّص منه (UnverifiableResponseError)، ويعود في طلب الشراء في صورة unknown. ولا تتحقق من استدعاءات المعلومات — ملفك التعريفي والمحافظ والكتالوج وبطاقاتك المملوكة (القائمة والقراءة): فردودها لا تحمل توقيعًا، وحماية TLS تكفيها. وما زالت توقّع كل استدعاء. راجع التحقق من الردود لمعرفة أي الردود موقَّعة.

طلبات الشراء

const price = card.unitPrice;
if (!price) throw new Error('This card has no price for this wallet.');

const operationId = crypto.randomUUID();                                   // yours
const request = { cardId: card.id, quantity: 2, expectedUnitPrice: price, expectedTotal: price.multiply(2) };
await saveOrderIntent(operationId, walletId, request);                     // store it BEFORE sending

const result = await client.orders.create(walletId, operationId, request);

if (result.kind === 'completed') await storeCredentials(result.credentials);   // store FIRST

switch (result.kind) {
  case 'completed':
    if (result.codesWithheld) await reportCodesWithheld(operationId);       // paid, codes withheld: do not buy again; write to support@anis.ly
    break;
  case 'replayed':
    await ensureStored(result.order);                                     // sent earlier: reveal by invoice if you never stored them
    break;
  case 'processing':
  case 'unknown':
    await resumeLater(operationId, result.suggestedDelayMs);             // may still complete: resume with the SAME id
    break;
  case 'notPlaced':
    await closeNotPlaced(operationId, result.refusal);                   // nothing bought: fix the cause, new order, new id
    break;
}

يرسل client.orders.resume(walletId, operationId, request) طلب الشراء نفسه مرة أخرى بالمعرّف نفسه — وهذه هي طريقة الاستعادة — ويعيد النتائج الخمس نفسها. والاستعادة دائمًا هي إعادة الإرسال الموقَّعة هذه، وليست حلقة من client.orders.get() التي تقرأ الحالة فقط. وطلب الشراء الذي حالته recoveryExhausted لم يفشل: واصل استئناف المعرّف نفسه ببطء (بالدقائق لا بالثواني)، وأخبر support@anis.ly بمعرّف العملية. راجع طلبات الشراء والاستعادة للاطلاع على القواعد كاملةً.

لا غلاف لإعادة المحاولة حول fetch

تعيد fetch التي تكرّر المحاولة إرسال الاستدعاء الموقَّع نفسه بقيمة الاستخدام الواحد نفسها، فترفض أنيس النسخة المكرَّرة. استعِد طلبات الشراء بـ orders.resume() التي توقّع استدعاءً جديدًا.

حالات الرفض

لا يرمي طلب الشراء الرفضَ استثناءً: يعود في صورة notPlaced أو unknown. أما بقية الاستدعاءات فتُرفض بـ AnisApiError (أو صنف فرعي منه مثل PriceChangedError أو InsufficientBalanceError) يحمل code وrequestId وretryAfter وisReplayed. وكل صفحة خطأ تذكر الرمز.

الخطأ المعنى في طلب الشراء
UnverifiableResponseError ردّ على استدعاء توقّع أنيس ردوده تعذّر إثبات أنه صادر من أنيس؛ فجرى التخلّص منه لا يُرمى — يعود unknown: استأنف بالمعرّف نفسه
LimitExceededError (spending_limit_exceeded، 429) كان الطلب سيتجاوز سقف إنفاق التطبيق أو المحفظة في النافذة الزمنية الحالية؛ لم يُشترَ شيء ولم يُخصم شيء لا يُرمى — notPlaced. بعد ثواني retryAfter قدّم طلبًا جديدًا بمعرّف جديد؛ ولا تستأنف هذا الطلب
RequestSigningError أخفق المُوقِّع لديك؛ لم يُرسَل شيء يُرمى — أرسله مرة أخرى بعد أن يعمل المُوقِّع
TypeError، RangeError مدخلاتك خاطئة (معرّف ليس UUID، أو إجمالي لا يساوي سعر الوحدة × الكمية)؛ لم يُرسَل شيء يُرمى قبل إرسال أي شيء

كل خطأ في الحزمة يمتد من AnisPartnersError، فيكفي فحص instanceof واحد لتغطيتها كلها.

المراقبة

مرّر أي مسجّل يملك debug وinfo وwarn وerror (يناسب ذلك pino وwinston وconsole) في الخيار logger؛ وتمرّ التتبّعات والمقاييس عبر OpenTelemetry تحت @anis-ly/partners. لا يُسجَّل أي مفتاح ولا توقيع ولا قيمة استخدام واحد ولا رمز بطاقة أبدًا. راجع المراقبة لمعرفة ما يستحق التنبيه عليه.

الدوال وردود البطاقات

تستخدم استدعاءات البطاقات المملوكة /v1/wallets/{walletId}/owned-cards، وقراءة /{soldCardId} وكشف /{soldCardId}/reveal، و/v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal. وتستخدم قراءة بطاقة الكتالوج GET /v1/wallets/{walletId}/catalog/cards/{cardId}.

تُستخدم CardSummary في البطاقات المملوكة والكشف وsoldCards في طلب الشراء: معرّف البطاقة واسمها المحلي، والقيمة المطبوعة والشعار الاختياريان، والفئة الفرعية والفئة. تستخدم البطاقة المملوكة soldCardId وoperationId. وتشمل البطاقة المكشوفة داخل طلب الشراء معرّف الفاتورة وملخص البطاقة ووقت الشراء أيضًا. تُحذف النصوص الفارغة والنصوص المحلية الفارغة. المال نص بثلاث خانات عشرية ورمز عملة من ثلاثة أحرف كبيرة — ولا يكون سالبًا إلا رصيد المحفظة حين تستخدم الدين المسموح؛ اقرأ عملة المحفظة من balance.currency.

تشترط الحزمة وسم التوقيع v2، وترويستي توقيع الاستدعاء في قائمة الأجزاء، وفحوص دورة حياة المفاتيح المنشورة. راجع التحقق من الردود. يسري الإيقاف فورًا؛ ولا يستأنف التطبيق إلا موظفو أنيس، وإلغاء مفتاحك الحالي يتطلب تسجيلًا جديدًا. راجع الأمان.

const reason = "Suspected compromise";
const catalogueCard = await client.catalogue.getCard(walletId, cardId);
const suspended = await client.access.suspendApplication(reason);
const revoked = await client.access.revokeKey(keyId, reason);

موارد المنتجات في SDK 2.2.0

يضيف الإصدار 2.2.0 من حزم SDK meterCharges وdirectRechargeProducts (المنتجات والمواقع وحساب السعر) وdirectRecharges (الإنشاء وقائمة عملياتك والقراءة وتعديل المستلم) وwebhookEndpoint وwebhookEvents وأداة للتحقق من توقيع الإشعارات وأنواع الأخطاء المحدّثة. حدّث من الإصدار 2.1.0 لاستخدامها. راجع أدلة العدادات والشحن المباشر والإشعارات لمعرفة سلوك HTTP. احفظ معرّف العملية ومحتواها كما سترسله قبل أي استدعاء ينقل مالًا؛ واستعد الردّ الضائع بالمعرّف نفسه. يذكر ملف README في كل حزمة وملف docs/webhooks.md فيها أسماء الدوال ومعاملات أداة التحقق.