يجب أن يعامل تكامل سلة عملية Refresh كتحول حالة له كاتب واحد لكل متجر. توثق سلة أن Refresh Token أحادي الاستخدام: كل تبادل ناجح يعيد رمز تحديث جديدًا ويلغي السابق. إذا حدّث عاملان المتجر نفسه بالتزامن، فقد تلغي إعادة استخدام الرمز القديم الوصول وتضطر التاجر إلى إعادة تثبيت التطبيق. النمط الآمن هو منسق واحد محدود، واستبدال ذري للرموز، وحالة تعافٍ واضحة عندما تكون نتيجة الشبكة غير محسومة.
هذا دليل هندسي عملي للتجارة السعودية، وليس خبرًا عن إصدار جديد من سلة. راجعت الوثائق الرسمية في ٥ أكتوبر ٢٠٢٦. القفل والمخطط والإطلاق المقترحة هنا أنماط داخل التطبيق، وليست خدمات جاهزة توفرها سلة.
افهم عقد سلة قبل تصميم إعادة المحاولة
تشرح وثائق Authorization في سلة وصول OAuth 2.0 وفق الصلاحيات التي يوافق عليها التاجر. وتذكر أن Access Token صالح لأسبوعين، وRefresh Token لشهر، وأن رمز التحديث يستخدم مرة واحدة. يعيد النجاح زوجًا جديدًا ويلغي السابق، وتحذر الوثائق من أن إعادة الاستخدام قد تلغي رموز الوصول وتستدعي إعادة تثبيت التطبيق.
هذا العقد يغيّر معنى Retry. تكرار قراءة كتالوج غالبًا لا يضر، أما تكرار طلب Refresh فهو حدث أمني. قد تؤذي Middleware عامة تعيد POST تلقائيًا بعد Timeout صلاحية المتجر. عطّل الإعادة العمياء على Token Endpoint، واجعل التحديث Workflow مسمى له حالة ومقاييس وسياسة تعافٍ مستقلة.
توضح الصفحة نفسها أن تطبيقات App Store المنشورة تستخدم Easy Mode، حيث تصل الرموز عبر أحداث التفويض إلى Webhook المخصص، بينما يستخدم Custom Mode للاختبار. طبّق وضع المنصة الحالي. كما تفرض إرشادات أمان Merchant API HTTPS وOAuth للوصول إلى بيانات المتجر الحساسة.
يبدأ السباق قبل انتهاء الصلاحية
تخيل عشرة Queue Workers يعالجون متجرًا واحدًا. قرؤوا صف الرموز نفسه ورأوا Access Token قريبًا من الانتهاء. قرر العاملان A وB التحديث. استبدل A الرمز R7 وحصل على A8 وR8. وقبل أن يحفظ R8 أرسل B الرمز R7. بما أن R7 استُهلك، ترى الخدمة إعادة استخدام لا طلبًا ثانيًا طبيعيًا.
لا تحل Transaction حول كل تحديث محلي هذا التسلسل إذا حدث الاتصال الخارجي خارج تنسيق مشترك. ولا يصلح Last-write-wins: لا يستطيع B الكتابة فوق نجاح A بخطأ، وقد تصل استجابة A بعد أن أعلن عامل آخر حاجة المتجر إلى إعادة التفويض. تحتاج العملية مالكًا واحدًا لكل تاجر وtoken_version متزايدًا دائمًا.
نفّذ التحديث قبل انتهاء Access Token بهامش، لكن أضف Jitter حتميًا لكل متجر كي لا تحدّث آلاف المتاجر في الدقيقة نفسها. وقد ينتج 401 عن أكثر من سبب؛ مرره عبر المنسق نفسه، واسمح بمحاولة Refresh واحدة مبنية على الدليل، ثم أظهر فشل التفويض الحقيقي.
احفظ سرًا ذا نسخة لا قيمتين منفصلتين
احفظ صفًا واحدًا لكل تاجر وتطبيق. حقول مفيدة: merchant_id وapp_id وencrypted_access_token وencrypted_refresh_token وaccess_expires_at وrefresh_expires_at وtoken_version وstatus وrefresh_lease_owner وrefresh_lease_until وupdated_at. ضع Unique Constraint على merchant_id مع app_id كي لا يصبح سجلان مصدرَي حقيقة لاتصال واحد.
شفّر الرمزين بمفتاح Envelope تديره خدمة مفاتيح خارج قاعدة البيانات. احصر فك التشفير في خدمة Refresh وAPI Client. لا تضع الأسرار في Queues أو URLs أو Analytics أو رسائل الاستثناء. يمكن للسجل حمل Merchant ID ونسخة الرمز وCorrelation ID ونوع النتيجة والزمن من دون الرمز أو Hash له. اطلب أقل Scopes يحتاجها المنتج.
يحمل كل عامل token_version التي قرأها. بعد انتظار تحديث عامل آخر، يعيد تحميل الصف: إذا زادت النسخة والحالة active يستخدم Access Token الجديد بدل التحديث مجددًا. ويجب أن تمر Webhooks الخاصة بالتثبيت أو app.updated عبر الكاتب الذري نفسه كي لا يتسابق الحدث مع التحديث المجدول.
نسّق Refresh واحدًا لكل متجر
يعمل Lease دائم عبر العمليات ويتحمل تعطل عامل. احجزه بعبارة شرطية واحدة: لا يتغير الصف إلا إذا كان Lease غائبًا أو منتهيًا، ثم يسجل Worker ID وموعد انتهاء قصير. إذا فشل الحجز فلا تتصل بسلة. انتظر قليلًا، ثم أعد تحميل الصف لترى هل رفع عامل آخر token_version أو نقل الاتصال إلى إعادة التفويض.
يقرأ المنسق Refresh Token المشفر والنسخة المتوقعة، ويرسل طلبًا واحدًا إلى Token Endpoint الموثق، ثم يحفظ الرمزين داخل Transaction قصيرة. يقارن التحديث Merchant ID وexpected token_version معًا. إذا فشلت المقارنة فلا تستخدم الاستجابة طبيعيًا؛ هناك كاتب آخر غيّر الصف أثناء الاتصال.
const lease = await acquireRefreshLease(merchantId, workerId, 30_000);
if (!lease.acquired) return await waitForNewerToken(merchantId, lease.version);
try {
const before = await loadEncryptedTokenState(merchantId);
const rotated = await sallaRefreshOnce(decrypt(before.refreshToken));
return await replaceTokensAtomically({
merchantId,
expectedVersion: before.version,
accessToken: encrypt(rotated.access_token),
refreshToken: encrypt(rotated.refresh_token),
nextVersion: before.version + 1
});
} catch (error) {
await classifyRefreshFailure(merchantId, error);
throw error;
} finally {
await releaseRefreshLease(merchantId, workerId);
}الكود تصور معماري لا SDK جاهزًا. استخدم الحقول ووحدات الزمن الموثقة والملاحظة لوضع سلة المختار. تحقق من ملكية Lease قبل الكتابة الأخيرة. اجعل Network Timeout أقصر من مدة Lease، ولا تجدده إلا بملكية صريحة، واجعل التحرير مشروطًا بـWorker ID كي لا يمسح عامل متأخر Lease لخليفته.
توفر PostgreSQL أيضًا Advisory Locks ذات معنى يحدده التطبيق. تستطيع تسلسل مفتاح المتجر، لكن قفل Session يعتمد على ملكية الاتصال، وقفل Transaction ينتهي معها. غالبًا يكون صف Lease أوضح مع Connection Pooler واتصال شبكة بعيد. وإذا استخدمت Advisory Lock فثبّت Session وحرره في finally وراقب pg_locks.
اعتبر الاستجابة الضائعة حالة أمنية غير محسومة
أصعب حالة هي Timeout بعد أن قبلت سلة R7 وقبل أن يتلقى العميل R8. إعادة R7 قد تفعّل اكتشاف إعادة الاستخدام، ولا يستطيع التطبيق إعادة بناء R8. القفل المحلي لا يصلح استجابة لم تصل. انقل الاتصال إلى refresh_unknown، وأوقف محاولات Refresh والعمليات الهدامة حتى تستعيد التفويض عبر مسار موثق.
لا تصنف كل Timeout بوصفه invalid_grant، ولا تطلب إعادة التثبيت فورًا قبل التحقق من أن منسقًا آخر حفظ نسخة أحدث. أعد تحميل الصف، وافحص Correlation والنسخة، واسمح بفترة محدودة لكاتب ناجح متأخر. إذا لم تظهر نسخة أحدث، اعرض للتاجر إجراء Reconnect واضحًا، واحتفظ بالعمل المعلق لإعادته بعد عودة التفويض.
يشرح RFC 9700 المقايضة وراء Refresh Token Rotation: لا يستطيع اكتشاف Replay معرفة هل المستخدم الشرعي أم المهاجم قدم الرمز الملغى، لذلك قد يلغي الرمز النشط لإيقاف الهجوم مقابل طلب تفويض جديد. هذه حماية، وليست خطأ مؤقتًا ينبغي هزيمته بمحاولة أخرى.
افصل تعافي التفويض عن Retry للأعمال
عندما لا يستطيع Worker الحصول على Access Token، أبقِ Business Job دائمة لكن محجوبة بالتفويض. لا تستهلك Retry Budget كل دقيقة. استخدم حالات active وrefreshing وrefresh_unknown وreauthorization_required وrevoked، واجعل Job Admission يفحصها قبل الاتصال بسلة.
بعد Reconnect، احفظ الزوج الجديد كنسخة جديدة، وارفع الحجب، ثم استأنف المهام عبر Idempotency Keys القائمة. ما زالت كتابات الطلب والشحنة والدفع تحتاج معالجة عدم اليقين الخاصة بها؛ الرمز الصالح لا يجعل Remote Mutation قابلة للتكرار. يشرح دليل تعافي Webhooks في سلة وزد المسار المنفصل للأحداث المتأخرة أو المفقودة.
تجنب Global Refresh Queue تجعل تاجرًا مكسورًا يعطل البقية. مفتاح التسلسل هو Merchant Connection، بينما تعمل سعة الطابور والتنبيهات على الأسطول كله. ضع حدًا أقصى للانتظار كي لا يحتجز Token Endpoint البطيء Request Threads إلى الأبد.
راقب التنسيق من دون تسريب الأسرار
قس محاولات التحديث والتدوير الناجح وتنافس Lease ومدة الانتظار وتقدم token_version واستجابات invalid_grant والنتائج غير المحسومة والمتاجر التي تنتظر إعادة التفويض. قسّم المقاييس بحسب نسخة التطبيق ونوع النتيجة، لكن لا تجعل Merchant ID غير المحدود Label للمقياس. استخدم Traces مضبوطة الصلاحيات للتحقيق الفردي.
محاولتا Refresh متزامنتان للنسخة نفسها تعنيان خلل تنسيق حتى لو نجحت إحداهما. انتهاء Lease المتكرر يشير إلى Timeout أو Crash. ارتفاع reauthorization_required بعد Deployment يجب أن يوقف الإطلاق. وراقب عمر Business Jobs المحجوبة كي لا يصبح فشل التفويض اختلافًا صامتًا في الطلبات.
يمكن لسجل Audit أن يذكر الخدمة والنسخة المتوقعة ونتيجة Lease ونوع استجابة المزود والنسخة المحفوظة وحالة التعافي. ولا يسجل Access Token أو Refresh Token أو Client Secret أو Authorization Header كاملة.
اختبر ترتيب الفشل قبل الإنتاج
اختبر عاملين متزامنين، وتعطلًا بعد حجز Lease، وTimeout قبل قراءة المزود للطلب، وآخر بعد استهلاك الرمز، واستجابة متأخرة بعد انتهاء Lease، وWebhook تفويض Easy Mode أثناء Refresh، وإزالة التطبيق. تحقق أن كل Refresh Token يقدم مرة واحدة، وأن نسخة قديمة لا تكتب فوق أحدث.
ثم اختبر طلبًا بدأ قبل التدوير، و401 أثناءه، ومهمة طلب معلقة بعد إعادة التفويض، وDeployment بنسختين من التطبيق. استخدم Demo Store وبيانات تجريبية كما توصي سلة؛ لا تختبر التعافي الهدام على طلبات تاجر حقيقي.
يستحق التصميم تطبيقه في Apps متعددة المتاجر وبها Workers متعددون أو Scheduled Jobs أو Webhooks متدفقة. يمكن لنموذج بعملية واحدة البدء بـMutex في الذاكرة، لكنه يتوقف عن الأمان بمجرد قدرة Process أو Region أو Runner أخرى على تحديث المتجر نفسه. العقد الإنتاجي واضح: يقدم Refresh Token مرة واحدة، ويحفظ بديله ذريًا، ويرى كل منتظر النسخة الجديدة، ويوقف عدم اليقين الأتمتة بدل المقامرة باتصال التاجر. وللتنفيذ، راجع خدمة هندسة تكاملات سلة وزد.
المراجع الرسمية
تدعم هذه المراجع سلوك الأدوات المذكورة. الأمثلة وقرارات التصميم توضيحية، ويجب تكييفها مع متطلبات المشروع وإصداراته.
إعداد: Noor Yasser
تعمل على تحدٍ تقني مشابه؟
أساعد الفرق على تحويل القرار المعماري إلى نطاق واضح وتنفيذ يمكن تشغيله ومراجعته بثقة.




