Files
tripz-llc/docs/19-entitlements-licensing.md
Hamza-AyedandClaude Opus 4.8 2edd8f6916 docs: نموذج الرصيد التشغيلي (18) والاستحقاقات (19) + تصحيحان على B
18 — الرصيد التشغيلي: السائق يشحن سلفاً، الراكب يدفع له الأجرة كاملة،
والعمولة تُخصم من الرصيد لا من الأجرة. مقارنة صريحة: هذا نموذج inDrive/
Yango/سيرو لا نموذج أوبر — وهو المعيار الفعلي في أسواق الكاش المستهدفة.
ثمنه ثلاثة التزامات: الحجب عند نفاد الرصيد (بدونه ينهار)، باقات لكل عملة،
ومعالجة حاجز دخول السائق الجديد.

19 — الاستحقاقات: علم الميزة في التطبيق قرار عرض لا حدّ أمني. الهندسة
العكسية تكشف شاشة تنادي نقطة ترجع 403. الحدّ الحقيقي FeatureGuard على
السيرفر + tenant_id من JWT موقَّع. الحقيقة التي تُقال صراحةً: ما يعمل كلياً
على الجهاز لا يُحمى، ووضع السيادة لا يُحمى تقنياً أصلاً (المستأجر يملك
السيرفر) — فليشمل سعره كل شيء بدل وهم حماية.

تصحيحان على المجموعة B (كلاهما بُني على افتراض خاطئ مني):
- B3: مشوار الوصول تعويضُ عدم حضور عند الإلغاء بعد 5 دقائق انتظار،
  لا بند في كل أجرة كما بنيته
- B6: price_for_driver = price_for_passenger — العمولة لا تُقتطع من الأجرة

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:32:32 +03:00

7.7 KiB

19 — الاستحقاقات (Entitlements) ومنع تفعيل الميزات بلا اشتراك

سؤال المالك (2026-07-17): «إذا نزّلنا التطبيق بشكل كامل، وجاء المشترك وعمل reverse engineering وفعّل الإضافات — كيف نحدّ منها؟»


1. الجواب في سطر واحد

لا تُفعَّل الميزة في التطبيق أبداً. تُفعَّل في السيرفر. التطبيق لا يملك ما يُسرَق: هو يعرض واجهة فقط. الميزة الحقيقية = نقاط API تحرسها المنصة.

2. القاعدة الحاكمة

علم الميزة في التطبيق (feature flag) هو قرار عرض، وليس حدّاً أمنياً — أبداً.

GET /tenant/config يرجع features كي يعرف التطبيق ماذا يُظهر. لو عدّل أحدهم الاستجابة أو فكّك التطبيق وفعّل كل الأعلام، فالنتيجة:

المهاجم يفعّل ميزة «البوتات» في التطبيق
   → تظهر له الشاشة والأزرار  ✅ (لا ضرر — بكسلات فقط)
   → يضغط زراً → POST /bots/campaigns
   → الحارس يقرأ tenant_id من الـJWT الموقَّع
   → يسأل: هل اشتراك هذا المستأجر يشمل bots؟  → لا
   → 403 Forbidden                              ❌ لا شيء حدث

كسب المهاجم: شاشة فارغة. هذا هو المطلوب بالضبط.

3. لماذا لا يستطيع تزوير tenant_id؟

  • tenant_id يأتي من JWT موقَّع بمفتاح السيرفر، لا من هيدر يكتبه العميل.
  • الترويسة x-tenant-id تُستعمل قبل الدخول فقط (اختيار المستأجر) — وبعد الدخول الحقيقة من التوكن.
  • تزوير التوكن يحتاج JWT_SECRET — وهو على السيرفر لا في التطبيق.

4. التطبيق العملي: حارس استحقاقات

@RequiresFeature('bots')      // ← الحدّ الأمني الحقيقي
@Post('bots/campaigns')
create(@CurrentUser() user: AuthUser, @Body() body: any) { … }

FeatureGuard:

  1. يقرأ tenant_id من التوكن.
  2. يجلب استحقاقات المستأجر — من Redis خط أول (نمط G4)، والقاعدة احتياط.
  3. غير مستحقّة → 403 برسالة واضحة (feature_not_in_plan) ليعرضها التطبيق بلطف.

كل نقطة تخصّ ميزة مدفوعة تحمل هذا الحارس. بلا استثناء واحد. نقطة واحدة منسيّة = الميزة مجانية للجميع. لذلك: الافتراض هو المنع — قائمة بيضاء لا سوداء.

5. الحد الذي لا يُتجاوز تقنياً

ما يعمل كلياً على الجهاز بلا نداء سيرفر — لا يمكن حمايته. نهائياً.

لا تشويش (obfuscation) ولا تشفير ولا فحص جذر يغيّر هذه الحقيقة؛ كلها ترفع الكلفة ولا تمنع. لذلك القاعدة المعمارية:

كل ميزة ذات قيمة تجارية يجب أن تمرّ بالسيرفر — ولو لم تحتج ذلك تقنياً.

مثال: «الاستخبار السوقي» لو حُسب في التطبيق من بيانات محليّة = مسروق بلا حيلة. ولو كان POST /market-intel/report = محميّ تماماً. هذا قرار تصميم يُتخذ عند بناء كل ميزة، لا ترقيع بعدها.

6. الثغرة الحقيقية: وضع السيادة (Sovereign)

هنا الخبر الذي يجب أن يُقال صراحةً:

في وضع السيادة (06-tenant-model) — المستأجر يشغّل نسختنا على خوادمه. عنده الكود والقاعدة والسيرفر. لا يوجد حارس يحرس ضدّه، لأنه هو صاحب الحارس.

يستطيع تعديل FeatureGuard ليرجع true دائماً. لا حلّ تقنيّ كامل. الخيارات الواقعية:

الخيار الجدوى
السيادة = كل الميزات، بسعرها ✅ الموصى به. لا شيء يُسرق لأن لا شيء محجوب. يطابق أصلاً كون «سيادة» أعلى الباقات ($599 + $12k إعداد).
مفتاح ترخيص موقَّع + اتصال دوري بالـControl Plane 🟡 يردع غير التقني، ويُنزع بتعديل الكود. مفيد للكشف والتوثيق العقدي لا للمنع.
مكوّن حرج يبقى عندنا (SaaS جزئي) 🟡 فعّال لكنه يناقض وعد السيادة نفسه (البيانات داخل الدولة).
العقد والقانون ✅ الحدّ الحقيقي في هذا الوضع. تدقيق + بند جزائي.

التوصية المعمارية: في الوضع المشترك (انطلاقة/علامة/أسطول+) الحماية تقنية وكاملة. في وضع السيادة الحماية تعاقدية، فليشمل سعرُها كلَّ شيء ولا نتظاهر بحمايةٍ لا نملكها.

7. تدفّق السوبر-أدمن: اشتراك ← استحقاقات

سوبر-أدمن ينشئ مستأجراً
   → يختار الباقة (launch | brand | fleet | sovereign)
   → يختار country_pack (jo | sy | eg)
   → الباقة تولّد استحقاقات افتراضية
   → + إضافات مشتراة منفردة (market_intel, bots, ads, transit…)
   → تُحفظ في tenants.features (jsonb — موجود أصلاً في الكيان)
   → تُبطَل من كاش Redis فوراً (نمط G4)
  • الميزة تُشترى منفردة فوق الباقة — لذا features ليست اشتقاقاً من plan بل قائمة صريحة. plan يعطي الافتراضات، وfeatures هي الحقيقة.
  • تغيير الاشتراك = تغيير features + إبطال الكاش → يسري خلال ثوانٍ بلا تحديث تطبيق.

8. شكل الاستحقاقات

// tenants.features
{
  "dispatch": true,
  "wallet": true,
  "bots": false,          // ← لم يشترِها
  "market_intel": false,
  "transit": false,
  "ads": false,
  "limits": { "drivers_max": 500, "cities_max": 3 }
}
  • الحدود العددية (limits) تُفرض على السيرفر أيضاً — سائق رقم 501 يُرفض بـ403.
  • الافتراض الصلب: مفتاح غير موجود = ممنوع (لا مسموح).

9. البنود المطلوبة (مجموعة K في الـ17-backend-backlog)

# البند
K1 FeatureGuard + @RequiresFeature() — استحقاقات من Redis، القاعدة احتياط
K2 كتالوج الميزات + استحقاقات افتراضية لكل باقة
K3 نقاط سوبر-أدمن: إنشاء مستأجر باشتراك، تعديل الاستحقاقات، إبطال الكاش
K4 فرض الحدود العددية (drivers_max, cities_max) على السيرفر
K5 GET /tenant/config يرجع features للعرض فقط — موثّق صراحةً أنه ليس حدّاً أمنياً
K6 اختبار عزل في CI: مستأجر بلا ميزة يأخذ 403 على كل نقاطها
K7 قرار وضع السيادة: كل الميزات مشمولة + بند تعاقدي (لا وهم حماية تقنية)

← ذو صلة: 06-tenant-model · 05-pricing-billing · 18-driver-credit-commission