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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f3fdc1bc20
commit
2edd8f6916
@@ -0,0 +1,111 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user