feat: باقي المجموعة I — أمان السحب (OTP + بصمة + HMAC + تدقيق)
I4 — OTP على السحب عبر نبيه. تدفّق خطوتين، وقاعدته: **لا يتحرك مال قبل إثبات الهوية**: - POST /payouts/request → يرسل رمزاً، حالة pending_otp، **بلا خصم** - POST /payouts/:id/confirm → يتحقق ثم يحجز ذرّياً (خصم I1) - حدّ 5 محاولات (رمز 4 خانات يُخمَّن في دقائق بلا حدّ)، الرمز يُستهلك مرة - complete يرفض طلباً بلا otp_verified_at (حارس ضد تخطّي التأكيد) - fail لا يردّ مالاً لطلب pending_otp — لم يُخصم منه شيء، وردّه يخلق مالاً I5 — biometric_method/at + device_id + request_ip على السحب. أثرٌ للتحقيق لا مصادقة: العميل يستطيع ادّعاءها، والمصادقة الحقيقية JWT + رمز واتساب. I6 — HMAC **مبنيّ ومطفأ** (PAYMENTS_REQUIRE_SIGNATURE=false) حتى يوقّع فلاتر؛ تفعيله الآن يقطع كل سحب. مفتاح **لكل جلسة** يُصدره الدخول لا سرّ ثابت في التطبيق (الثابت يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية). يوقّع timestamp.METHOD.path.body بنافذة 5 دقائق؛ rawBody مفعّل في main. I7 — tripz_audit_log append-only: من·ماذا·متى·أي IP وجهاز. لا يرمي أبداً — فشل التدقيق يجب ألّا يُسقط عمليةً مالية نجحت. I2 أُلغيت الحاجة إليها: إيراد المنصة = الشحن، و credit_txns دفتره فعلاً؛ محفظة ثانية = دفتر مزدوج يحتاج مطابقة. الباقي تقرير لا محفظة. I3/I8 مؤجَّلتان بوعي (توثيق في docs/17). wallet-race-test.mjs حُدِّث: السباق انتقل من request إلى confirm، وأُضيف محكّ أن الطلب وحده لا يمسّ الرصيد. هجرة: PayoutSecurityAndAudit (تعتبر السحوبات القائمة مُتحقَّقة وإلا رفض complete صرفها للأبد). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
91e1692457
commit
1605754722
@@ -166,13 +166,20 @@
|
||||
| # | البند | التفصيل |
|
||||
|---|-------|---------|
|
||||
| I1 | ✅ **إصلاح سباق المحفظة** | **منفَّذ ومُثبَت على السيرفر** (commit 9d6b752): `UPDATE … SET balance = balance ± :delta WHERE tenant_id … AND balance >= :amount RETURNING *` — عبارة واحدة ذرّية، والقيد+الرصيد في معاملة واحدة. أُضيف `wallet_txns.balance_after` وقيد `CHECK (balance >= 0)` كشبكة أمان. إنشاء المحفظة عبر `ON CONFLICT DO NOTHING`.<br>**نتيجة `wallet-race-test.mjs 100 5` على Postgres حقيقي:** 100 خصم متزامن → نجح 50 بالضبط، رُفض 50، الرصيد 250→0، المخصوم = 250 (لا مال ضائع ولا مخلوق)، الزمن 1492ms. ✅ |
|
||||
| I2 | **محفظة لكل طرف + محفظة المنصة** | راكب · سائق · المستأجر/المنصة (مكافئ `siroWallet`) — أساس العمولة (B6). |
|
||||
| I3 | **جدول لكل طريقة دفع** | شام كاش · كليك · إي كاش · بيموب · MTN · فوري … لكل واحدة جدولها + محوّل (adapter) موحّد. حالياً عندنا `tripz_pay_payments` عام. |
|
||||
| I4 | **OTP على الـpayout عبر نبيه** | إرسال كود + تحقّق قبل تنفيذ السحب (لا يوجد في سيرو). |
|
||||
| I5 | **بصمة (وجه/إصبع) في فلاتر** | تأكيد حيوي قبل السحب — يُربط بالطلب (device fingerprint من D2). |
|
||||
| I6 | **HMAC على العمليات المالية** | توقيع الطلب (D3) — إلزامي على topup/payout. |
|
||||
| I7 | **سجل تدقيق مالي** | مكافئ `admin_audit_log` — من فعل ماذا ومتى على كل عملية. |
|
||||
| I8 | **webhook SMS + Gemini** | تبنّي نمط سيرو للأسواق بلا API رسمي (سوريا): `raw_sms_log` + استخراج بالـAI + تسوية. |
|
||||
| # | البند | الحالة |
|
||||
|---|-------|--------|
|
||||
| I4 | **OTP على الـpayout عبر نبيه** | ✅ تدفّق خطوتين: `POST /payouts/request` يرسل رمزاً **بلا حركة مال**، و`POST /payouts/:id/confirm` يتحقق ثم يحجز ذرّياً. حدّ 5 محاولات، الرمز يُستهلك مرة واحدة، و`complete` يرفض طلباً بلا `otp_verified_at`. **قاعدة: لا يتحرك مال قبل إثبات الهوية.** |
|
||||
| I5 | **بصمة (وجه/إصبع)** | ✅ الجزء الخلفي: `biometric_method`/`biometric_at` + `device_id` + `request_ip` تُسجَّل مع السحب. **أثرٌ للتحقيق لا مصادقة** — العميل يستطيع ادّعاءها؛ المصادقة الحقيقية JWT + رمز واتساب. الإثبات الحيّ نفسه في فلاتر. |
|
||||
| I6 | **HMAC على العمليات المالية** | 🟡 **مبنيّ ومطفأ** (`PAYMENTS_REQUIRE_SIGNATURE=false`) — يحتاج فلاتر أن يوقّع أولاً، وتفعيله قبل ذلك يقطع كل سحب. **مفتاح لكل جلسة** يُصدره الدخول (`signing_key`) لا سرّ ثابت في التطبيق (الثابت يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية). يوقّع `timestamp.METHOD.path.body` بنافذة 5 دقائق. |
|
||||
| I7 | **سجل تدقيق مالي** | ✅ `tripz_audit_log` (append-only): من · ماذا · متى · من أي IP وجهاز. يغطّي `payout.request/confirm/complete/fail` و`otp_failed`/`otp_blocked` و`signature.rejected`. **لا يرمي أبداً** — فشل التدقيق لا يُسقط عمليةً نجحت. |
|
||||
| I2 | **محفظة المنصة** | ⏳ **أُلغيت الحاجة إليها عملياً**: بعد نموذج الرصيد التشغيلي (docs/18) لم تعد المنصة تقبض عمولة من كل رحلة — إيرادها = **الشحن**، و`credit_txns` هو دفتر ذلك الإيراد فعلاً (`SUM` الشحن لكل مستأجر). محفظة ثانية = مسك دفتر مزدوج يحتاج مطابقة. البند الباقي = **تقرير إيراد** لا محفظة. |
|
||||
| I3 | **جدول لكل طريقة دفع** | ⏳ لم يُنفَّذ — إعادة هيكلة مخطط كاملة (`tripz_pay_payments` عام حالياً). يُنفَّذ مع أول ربط بوابة حقيقية، لا قبله. |
|
||||
| I8 | **webhook SMS + Gemini** | ⏳ لم يُنفَّذ — ميزة كاملة (`raw_sms_log` + استخراج + تسوية). تُبنى عند دخول السوق السوري فعلياً. |
|
||||
|
||||
### 🔴 أخطر ثغرة مالية قائمة — ليست في الكود
|
||||
**الـAPI يعمل على `http` بلا TLS** (`194.163.173.157:4010`، والتطبيق يفعّل `usesCleartextTraffic=true`). من يلتقط الشبكة يسرق توكن أي سائق ويسحب أرباحه — ولا OTP ولا HMAC ولا تدقيق يمنع ذلك تماماً.
|
||||
- توقيع I6 **يخفّف** الضرر (التوكن المسروق وحده لا يكفي: المفتاح لا يمرّ في الشبكة بعد الدخول) لكنه **ترقيع لا بديل**.
|
||||
- **الإصلاح الحقيقي: TLS** (نطاق + شهادة عبر CloudPanel/Let's Encrypt) ثم `usesCleartextTraffic=false`. **يسبق أي عمل مدفوعات آخر.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user