Files
tripz-llc/docs/19-entitlements-licensing.md
T
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

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)