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
@@ -52,16 +52,35 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// كل الطلبات تنطلق دفعة واحدة على نفس الصف.
|
||||
console.log(`إطلاق ${ATTEMPTS} خصم متزامن × ${AMOUNT} (يكفي ${expectedSuccesses} فقط)…`);
|
||||
// تدفّق السحب صار خطوتين (docs/17 — I4): الطلب يرسل رمزاً ولا يخصم شيئاً،
|
||||
// والخصم يقع عند التأكيد. فالسباق هنا على `confirm` لا على `request`.
|
||||
// يعتمد dev_code الذي ترجعه النقطة في وضع التطوير (OTP_DEV_MODE=true).
|
||||
console.log(`تحضير ${ATTEMPTS} طلب سحب (بلا خصم)…`);
|
||||
const requested = await Promise.all(
|
||||
Array.from({ length: ATTEMPTS }, () =>
|
||||
api('POST', '/payouts/request', t, { amount: AMOUNT, channel: 'cliq' }).catch(() => null),
|
||||
),
|
||||
);
|
||||
const pending = requested.filter((r) => r?.ok && r.body?.payout?.id && r.body?.dev_code);
|
||||
if (pending.length !== ATTEMPTS) {
|
||||
console.log(`⚠️ حُضّر ${pending.length}/${ATTEMPTS} فقط.`);
|
||||
if (pending.length === 0) {
|
||||
console.log('لا شيء لتأكيده — تأكّد أن OTP_DEV_MODE=true (يُرجع dev_code).');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// فحص وسيط: الطلب وحده يجب ألّا يمسّ الرصيد إطلاقاً.
|
||||
const mid = await api('GET', '/wallet', t);
|
||||
const midBalance = Number(mid.body.balance);
|
||||
|
||||
console.log(`إطلاق ${pending.length} تأكيد متزامن × ${AMOUNT} (يكفي ${expectedSuccesses} فقط)…`);
|
||||
const t0 = Date.now();
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: ATTEMPTS }, () =>
|
||||
api('POST', '/payouts/request', t, { amount: AMOUNT, channel: 'cliq' }).catch((e) => ({
|
||||
ok: false,
|
||||
status: 0,
|
||||
body: { message: String(e) },
|
||||
})),
|
||||
pending.map((r) =>
|
||||
api('POST', `/payouts/${r.body.payout.id}/confirm`, t, {
|
||||
code: String(r.body.dev_code),
|
||||
}).catch((e) => ({ ok: false, status: 0, body: { message: String(e) } })),
|
||||
),
|
||||
);
|
||||
const elapsed = Date.now() - t0;
|
||||
@@ -80,8 +99,8 @@ async function main() {
|
||||
console.log(`الزمن : ${elapsed}ms`);
|
||||
console.log('');
|
||||
|
||||
// المحكّات الثلاثة
|
||||
const checks = [
|
||||
['الطلب وحده لا يمسّ الرصيد (لا مال قبل إثبات الهوية)', midBalance === startBalance],
|
||||
['الرصيد لم يصبح سالباً', endBalance >= 0],
|
||||
['عدد النجاحات = ما يسمح به الرصيد', ok === expectedSuccesses],
|
||||
['المخصوم = النجاحات × المبلغ (لا مال ضائع/مخلوق)', Math.abs(spent - ok * AMOUNT) < 1e-6],
|
||||
|
||||
Reference in New Issue
Block a user