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>
112 lines
7.7 KiB
Markdown
112 lines
7.7 KiB
Markdown
# 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)
|