# 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. التطبيق العملي: حارس استحقاقات ```ts @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](06-tenant-model.md)) — المستأجر يشغّل نسختنا على خوادمه. عنده الكود والقاعدة والسيرفر. لا يوجد حارس يحرس ضدّه، لأنه هو صاحب الحارس.** يستطيع تعديل `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. شكل الاستحقاقات ```jsonc // 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](17-backend-backlog.md)) | # | البند | |---|-------| | K1 | `FeatureGuard` + `@RequiresFeature()` — استحقاقات من Redis، القاعدة احتياط | | K2 | كتالوج الميزات + استحقاقات افتراضية لكل باقة | | K3 | نقاط سوبر-أدمن: إنشاء مستأجر باشتراك، تعديل الاستحقاقات، إبطال الكاش | | K4 | فرض الحدود العددية (`drivers_max`, `cities_max`) على السيرفر | | K5 | `GET /tenant/config` يرجع `features` **للعرض فقط** — موثّق صراحةً أنه ليس حدّاً أمنياً | | K6 | اختبار عزل في CI: مستأجر بلا ميزة يأخذ 403 على كل نقاطها | | K7 | قرار وضع السيادة: كل الميزات مشمولة + بند تعاقدي (لا وهم حماية تقنية) | --- ← ذو صلة: [06-tenant-model](06-tenant-model.md) · [05-pricing-billing](05-pricing-billing.md) · [18-driver-credit-commission](18-driver-credit-commission.md)