Asset Vault

دراسة حالةمنصة فواتير ومخزونمشروع 02

مُختبَر · تكامل مستمر ناجح · إصلاحات التحكم بالوصول مدموجة 2026-10-24 · غير منشور (عمل ضمن معرض أعمالي) · مستودع عام

لقطة شاشة · بيانات اصطناعية شاشة الفواتير في Asset Vault بالوضع الداكن، ببيانات اصطناعية

الملخص

Asset Vault منصة للفوترة والمخزون وإدارة المتاجر المتعددة. قيمتها كدليل هندسي لا تكمن في قائمة الميزات بقدر ما تكمن في حملة التحصين المقيسة، إيداعًا بإيداع، بين 2026-08-31 و2026-09-07، وفي القرارات التي اتُّخذت في أثنائها، ومنها حذف ميزة بدل وصفها بأنها تعمل.

المشكلة والمستفيدون

يدير تجار التجزئة الصغار متعددو المتاجر الفوترة والمخزون وعمليات المتاجر في أدوات منفصلة: إجماليات لا تطابق الدفاتر، ومخزون يتباعد، ولا رؤية موحّدة للأعمال.

تجار التجزئة الصغار الذين لديهم أكثر من موقع ويحتاجون إلى الفواتير والمخزون والتحويلات في مكان واحد. عمل ضمن معرض أعمالي قابل للاستضافة الذاتية؛ لا نسخة مستضافة له ولا مستخدمين في الإنتاج.

دوري

عمل فردي. أملك نطاق المنتج، ونموذج البيانات، وترحيل الهوية من JWT مكتوب يدويًا إلى مزوّد مُدار، وحملة التحصين. كان التنفيذ بمساعدة الذكاء الاصطناعي تحت توجيهي؛ ونُفّذت حملة التحصين خصوصًا خطوة بخطوة مع إعادة التحقق من عدد أخطاء الأنواع وعدد الاختبارات بعد كل إيداع، وتلك الأعداد مسجّلة في نصوص الإيداعات.

البنية

كل حركة المتصفح عبر REST. توجد طبقة tRPC على الخادم (4 موجّهات و15 إجراءً) لكن الواجهة المطروحة لا تستدعيها، ولهذا لم تعد هذه الصفحة تصف المنصة بأنها قائمة على tRPC. البيانات محصورة لكل مستخدم: يحمل كل سجل معرّف مالكه. توجد ميزة عضوية المؤسسات لكنها لا تقسّم البيانات.

القرارات والمفاضلات

  • هوية مُدارة بدل مصادقة مكتوبة يدويًا. تحذف ترحيلة أعمدة كلمة المرور ورمز إعادة التعيين والإقفال وتضيف معرّف مستخدم Clerk؛ ويرفض التطبيق العرض دون المفتاح القابل للنشر.
  • حذف المصادقة الثنائية (2FA) الزائفة. كان مربع حوار 2FA يعيد النجاح دون أن يخزّن شيئًا، ولا يمكن لعلم في جدول التطبيق نفسه أن يحرس تسجيل دخول بهوية مُدارة. بعد أن ربطته أولًا، أدركت أنه لا يُصلَح في مكانه فحذفت 555 سطرًا، مع إبقاء العمود لترحيلة لاحقة.
  • فصل البناء عن فحص الأنواع، ثم جعله بوابة. كان سكربت البناء tsc -b && vite build، ولا يُخرج شيئًا مع وجود 407 أخطاء. قُسّم إلى بناء / فحص أنواع / بناء صارم؛ وكان العدد يُبلَّغ عنه دون أن يوقف البناء حتى وصل إلى الصفر، ثم أصبح بوابة في التكامل المستمر. 407 ← 0 في الواجهة و12 ← 0 في الخلفية، دون أي إخماد ودون any.
  • معالجة الاعتماديات دون إجبار. 195 تحذيرًا أمنيًا، منها 5 حرجة، بينها تجاوز لحماية المسارات في مكتبة المصادقة المثبّتة. ترقية المكتبة، وإصلاح التدقيق دون --force، وتأجيل الإصدارات الرئيسية الكاسرة لاختبار منفصل، وإعادة فحص البناء والاختبارات بعد كل خطوة: الخلفية 69 ← 31 تحذيرًا (الحرجة 2 ← 0)، والواجهة 24 ← 6.
  • التحقق كبيانات في أثناء العرض. كان التحقق من المبالغ المالية في الواجهة يرمي استثناءً عند مدخلات ترفضها الخلفية، فغُيّر ليعيد أحكامًا، لأنه يعمل داخل memo في أثناء العرض. الحسابات لم تُمسّ؛ و19 اختبارًا.
  • تنفيذ واحد لحسابات الفواتير المالية. كان التحقق من مبالغ الفواتير وحسابها قد تباعد بين نسخ تُزامَن يدويًا في الواجهة البرمجية وتطبيق الويب ومسار إرسال نموذج الفاتورة. وهي الآن تتشارك وحدة واحدة بلا اعتماديات (طلب السحب #6، إيداع الدمج e1dabf3، 2026-10-31). وكشف توحيدها ذلك التباعد: كانت المبالغ الواقعة على نصف سنت تمامًا تُقرَّب أحيانًا إلى الأسفل خلافًا للقاعدة الموثّقة (التقريب بعيدًا عن الصفر)، والحساب الآن بأعداد صحيحة دقيقة فلا تتغير المبالغ إلا بسنت واحد في تلك الحالات وحدها؛ وكانت رسائل التحقق في تطبيق الويب تختلف عن رسائل الواجهة البرمجية؛ وكان ملخّص النموذج قد يعرض إجماليات قديمة. ثم أكمل طلب سحب لاحق (#8، إيداع الدمج d213e7e) ذلك: صار كل مُدخل وكل مبلغ محسوب محدودًا بما تتسع له أعمدة numeric(12,4)، فلا يكتب الإنشاء أو التحديث الذي يتجاوز الحد شيئًا؛ وبقيت نسب الضريبة والخصم بأربع منازل عشرية في الحساب والتخزين وإعادة التحميل والتعديل (ترحيلة واحدة توسّع عمودَي النسب)؛ وصار البند الذي يحمل بيانات بلا وصف يمنع الإرسال بدل أن يُسقَط بصمت. وأصلح أيضًا تحديث الفواتير التي تتضمن بنودًا، إذ كان يفشل على main.
  • اختبارات حارسة تحلّل الشيفرة المصدرية. كانت قائمة حذف قديمة تُسقِط بصمت 21 حقلًا من 26 حقل إعدادات حقيقيًا. يستخرج الإصلاح بانيًا للحمولة مع 40 اختبارًا وحارسًا يستورد الملف المصدري نصًا ويحلّل حقوله المعلنة؛ وجرى التحقق منه بإعادة إدخال الخطأ (يفشل 6 اختبارات).
  • مراجعة التحكم بالوصول، بإثبات على قاعدة بيانات حقيقية. كشفت إعادة اختبار التطبيق على قاعدة PostgreSQL حقيقية أن حارس المصادقة كان يفكّ الرموز دون التحقق من توقيعها، وأن أي حساب مسجّل الدخول كان يستطيع عرض كل الحسابات الأخرى أو تعديلها أو حذفها، وأن مفاتيح التخزين لم يكن لها فحص ملكية، وأن عضوية المؤسسة كان يمكن لغير الأعضاء إعادة تفعيلها أو رفع صلاحياتها (وكان المشرف يستطيع أيضًا إزالة المالك الوحيد). أُعيد إنتاج كل منها أولًا، ثم أُصلح مع اختبار ضبط إيجابي بجانب كل اختبار رفض. ولأن Clerk مُحاكى في تلك الحزمة، يشغّل اختبار منفصل حزمة SDK الحقيقية دون اتصال بالشبكة وبمفاتيح مولَّدة؛ وقد كشف أيضًا عدم تطابق في شكل النتيجة كان سيرفض كل مستخدم صالح. الإصلاحات مدموجة في المستودع، لا منشورة. والتراجع عنها يعيد الثغرات، لذا فإن إرشادات التعافي في README هي إبقاء الإصدار الفاشل خارج الخدمة والإصلاح للأمام، لا الرجوع أبدًا إلى ما قبل هذا التغيير.

نتائج مُتحقَّق منها

المقياسقبل ← بعدالدليل
أخطاء TypeScript الصارمة407 ← 0 في الواجهة؛ 12 ← 0 في الخلفيةسلسلة الإيداعات ac8d6c2 … 4a77e83 وb94106a؛ فحص الأنواع بوابة في التكامل المستمر (57a2a36)
اختبارات الخلفية42 ناجحًا / 30 فاشلًا ← 147/147 ← 160/160الإيداعات 882f947 … 0e03727؛ تشغيل التكامل المستمر 34171675761 بتاريخ 2026-09-07: 160 ناجحًا في 14 حزمة. هذه الاختبارات تحاكي قاعدة البيانات.
اختبارات التحكم بالوصول على PostgreSQL حقيقيةلا شيء ← 45/45 (فشل 16 من أول 42 اختبارًا على الشيفرة الأصلية)إيداع الدمج 93bdb9c؛ تشغيل التكامل المستمر 40120871037 بتاريخ 2026-10-24. بُني المخطط من الترحيلات الـ 40؛ والتحقق من Clerk مُحاكى في هذه الحزمة
اختبارات الرموز بحزمة Clerk SDK الحقيقيةلا شيء ← 13/13 (دون اتصال، بمفاتيح مولَّدة)التشغيل نفسه؛ حزمة اختبارات الوحدة للخلفية 173 ناجحًا في 15 حزمة
اختبارات حسابات الفواتير على PostgreSQL حقيقيةلا شيء ← 8 (طلب السحب #6؛ على الشيفرة السابقة تفشل حالتا نصف السنت بالضبط) ← 14 (طلب السحب #8: حدود التخزين، ونسب بأربع منازل عشرية، والكتابة المرفوضة لا تترك أثرًا)تشغيل التكامل المستمر 41232428609 على main f9bcc74 بتاريخ 2026-10-31: نجحت حزم قاعدة البيانات الحقيقية بـ 111 اختبارًا في 9 ملفات، منها 48 للتحكم بالوصول و14 لحسابات الفواتير
اختبارات الواجهةالحزمة لا تُصرَّف (compile) ← 98/98 ← 126/126الإيداعات 24279fb … cd7f0b9؛ العدد الحالي من تشغيل التكامل المستمر 41232428609
صور الحاوياتلا تُبنى في التكامل المستمر ← تُبنى الصورتان في التكامل المستمر؛ وتُشغَّل صورة الواجهة ويُجرى عليها فحص سريعطلب السحب #7؛ تشغيل التكامل المستمر 41232428609: يُرفض البناء دون مفتاح الهوية، وتقدّم الحاوية العاملة التطبيق للرابط العميق /invoices/create، وتعيد 404 لملف مفقود، وتقدّم JavaScript مضغوطًا، ويُتحقَّق من صحة ملف Compose
ترحيلات الإصدارلم تكن صورة الإنتاج قادرة على تشغيل ترحيلاتها، وكانت الواجهة البرمجية تبدأ على أي مخطط ← تشغّل الصورة الترحيلات المُصرَّفة خطوةً منفصلة، وترفض الواجهة البرمجية البدء ما دامت هناك ترحيلة معلّقةطلب السحب #10؛ مهمة في التكامل المستمر على PostgreSQL مؤقتة، التشغيل 41232428609: يُرفض المخطط القديم، ويطبّق أمر الصورة الموثّق الترحيلة، ولا تغيّر إعادة التشغيل شيئًا، وتوقف الترحيلة الفاشلة الإصدار قبل بدء الواجهة البرمجية، وتبقى قيم الفواتير وإجمالياتها كما هي
المنظومة كاملة في التكامل المستمرلا تُشغَّل معًا ← صورة الواجهة البرمجية في وضع الإنتاج مع PostgreSQL مرحَّلة والواجهة المبنيةطلب السحب #11؛ التشغيل 41232428609: 28 فحصًا للواجهة البرمجية مع تحقق حقيقي من رموز RS256 (مفاتيح تُولَّد لكل تشغيل). استعلام مستخدم Clerk مُحاكى، وتسجيل الدخول في المتصفح غير مشمول
JavaScript الأولي652 ← 314 كيلوبايت مضغوطًا (2,348 ← 1,030 كيلوبايت دون ضغط)طلب السحب #11؛ تقسيم الشيفرة حسب المسار، بإعداد البناء نفسه قبل وبعد؛ يُتحقَّق في التكامل المستمر من أن لكل مسار من المسارات الـ 36 جزءًا خاصًا به، ويعرض الجزء الذي يفشل تحميله شاشة إعادة تحميل
مُدخلات كانت تُتجاهلأربعة مُدخلات في الواجهة البرمجية كانت تُقبل ثم تُتجاهل (مرشّح المتاجر النشطة، تقييم المخزون لكل متجر، واختبار اتصال SMTP كان يستخدم بيانات اعتماد مخزّنة، والكائنات كانت تتحول إلى "[object Object]") ← كلٌّ منها يُحترم أو يُرفض، مع فحوص الملكيةطلب السحب #16؛ اختبارات انحدار على PostgreSQL حقيقية تفشل على الشيفرة السابقة (1/5 و5/8 و26/32 حمراء قبل الإصلاح)
سلوك الخادمكان الأصل غير المسموح في CORS يعيد 500؛ وكانت إشارة SIGTERM تُتجاهل (يُقتل بعد 10 ثوانٍ)؛ وكانت إعادة التوجيه إلى HTTPS تُبنى من ترويسة Host؛ وكانت ترويسات X-Forwarded-* تُصدَّق من أي طرف ← 403 بصيغة JSON؛ وتفريغ تدريجي محدود؛ وإعادة توجيه إلى الأصل المُعدّ فقط؛ ولا يُوثق بالوكلاء إلا عند إعدادهمطلب السحب #14؛ قيس داخل حاوية الواجهة البرمجية في التكامل المستمر: اكتمل الطلب الجاري، ورُفضت الاتصالات الجديدة بعد 54 مللي ثانية من الإشارة، وخرجت الحاوية بالرمز 0 خلال 2.3 ثانية مقابل حد 8 ثوانٍ؛ وتُحقّق من منظومة Compose خلف وكيل TLS من طرف إلى طرف
التدقيق اللغوي للخلفية1,624 خطأ و289 تحذيرًا، والخطوة غير حاجزة ← 0 و0، حاجزةطلبات السحب #13 و#15 و#17 و#18؛ أنواع دقيقة عند الحدود، ولم تُعطَّل أي قاعدة، ولم يُستثنَ أي ملف، وأربعة استثناءات من سطر واحد لكلٍّ منها سبب
عيوب كشفتها الأنواعكانت مبيعات الأصناف غير النشطة تبلّغ عن النجاح دون تغيير المخزون؛ وكانت حسابات مزوّد الهوية المقفلة تُقبل؛ وكانت إحصاءات الفواتير 0 دائمًا؛ وكانت ملاحظة طريقة الدفع المحفوظة و19 إعدادًا محفوظًا تُسقَط بصمت؛ وكان اختبار SMTP الجديد قادرًا على الوصول إلى عناوين داخلية ← أُصلح الجميعطلب السحب #18، التشغيل 41232428609؛ لكل إصلاح إيداعه الخاص مع اختبار يفشل على الشيفرة السابقة؛ وجاءت ملاحظة SMTP من المراجعة المستقلة وأُغلقت باختبار 60 صيغة لكتابة العناوين
اختبار اتصال SMTPكانت المقابس قد تبقى بعد النتيجة؛ وكان يمكن أن تُرسَل بيانات الاعتماد عبر TCP غير مشفّر؛ وكان نص المضيف يصل إلى السجلات؛ ولم يكن للمسار حدّ معدّل؛ وكانت سجلات جسم الطلب تحجب أسماء المفاتيح المطابقة حرفيًا فقط ← مقبس مملوك يُدمَّر عند مهلة واحدة، واشتراط TLS، والتحقق من المضيف، وحدود لكل مستخدم ولكل عنوان وللمحاولات الجارية، وحجب بيانات الاعتماد تحت أي اسم مفتاحطلب السحب #19، التشغيل 41232428609؛ كشفتها مراجعة مستقلة للمسار وفحص الحزمة الجديدة لغياب الأسرار من السجلات؛ وأُعيدت مراجعته عند الإيداع المدموج دون ملاحظات
البناء من نسخة نظيفةمعطّل (لا مخرجات مع وجود أخطاء أنواع؛ مدخل إنتاج خاطئ؛ أمثلة البيئة تنقصها مفاتيح) ← يُبنىالإيداعات ac8d6c2 و21db0e4 و0fde86e
تحذير حرجتجاوز حماية المسارات في مكتبة المصادقة ← مُعالَجالإيداع 40c15d5، 2026-08-31
تنبيهات الاعتماديات الحرجة2 مفتوحان (proxy-addr وvitest) ← مُرقَّعان على main؛ 0 حرج في npm auditإيداع الدمج 93bdb9c؛ كانت تنبيهات GitHub ما تزال مدرجة مفتوحة بعد الدمج بقليل، بانتظار إعادة الفحص
التكامل المستمرلا شيء ← ناجح؛ 48 من آخر 49 تشغيلًا على main ناجحة في 2026-10-31GitHub Actions منذ 2026-08-31

القيود والعمل المتبقي

  • غير منشور. يشغّل التكامل المستمر صورة الواجهة البرمجية على PostgreSQL مرحَّلة مع الواجهة المبنية، لكن استعلام مستخدم Clerk مُحاكى هناك وتسجيل الدخول في المتصفح غير مُختبَر؛ وتغطيته تحتاج إلى نسخة تطوير من Clerk ومفاتيحها أسرارًا في التكامل المستمر ومستخدم اختبار. ولا توجد نسخة مستضافة ولا مستخدمون في الإنتاج.
  • اختبارات الخلفية الأصلية الـ 160 تحاكي قاعدة البيانات. وتضم حزم PostgreSQL الحقيقية في التكامل المستمر 111 اختبارًا في 9 ملفات: 48 للتحكم بالوصول، و14 تُنشئ فواتير وتحدّثها عبر الواجهة البرمجية وتتحقق من الإجماليات والحدود والنسب المخزّنة، والباقي لمبيعات المخزون، وترشيح المتاجر وتقييمها، واختبار اتصال SMTP وحدود معدّله، والإعدادات المحفوظة، ومنظومة HTTP (CORS، والثقة بالوكيل، والإيقاف). والتحقق من رموز Clerk مُحاكى فيها؛ ويشغّل اختبار منفصل حزمة SDK الحقيقية دون اتصال بالشبكة. ولم يُشغَّل أي منهما على نسخة Clerk حية.
  • البيانات محصورة لكل مستخدم. توجد ميزة عضوية المؤسسات لكنها لا تقسّم البيانات؛ وإضافة عضو بمعرّفه لا تحتاج إلى موافقة ذلك المستخدم. وكشفت مراجعة للتحكم بالوصول ثغرة في التحقق من الرموز وثلاث ثغرات وصول بين الحسابات وأُصلحت (دُمجت 2026-10-24). ثم كشف فحص المنظومة كاملة ومراجعة نهائية أن إنشاء فاتورة كان يقبل عميلًا أو صنف مخزون لمستخدم آخر ويعيد ذلك السجل؛ وأُصلح كلاهما مع اختبارات (دُمجا 2026-10-31).
  • يمكن إنشاء مفاتيح واجهة برمجية لكنها لا تصادق على شيء، والوسيط الخاص بالسماح/الحظر حسب عنوان IP لا يُسجَّل أبدًا، ويرفض معترِض تفاوض المحتوى الرفع متعدد الأجزاء.
  • كلا التنبيهين الحرجين للاعتماديات مُرقَّع على main؛ وتبقى ملاحظات تدقيق عالية الخطورة متابَعة لا مُصلَحة.
  • تُخزَّن كل العملات بمنزلتين عشريتين؛ وهذا موثّق بوصفه قيدًا، لا مُعالَج.
  • يتصل اختبار اتصال SMTP (نقطة نهاية في الواجهة البرمجية تتطلب تسجيل الدخول) بمضيف ومنفذ يدخلهما المستخدم. وهو يشترط TLS قبل إرسال بيانات الاعتماد، ويرفض عناوين الاسترجاع والعناوين الخاصة وعناوين الوصلة المحلية والعناوين التي تضمّن IPv4، ولا يتصل إلا بالعنوان الذي تحقّق منه، وينهي المحاولة كلها عند مهلة محددة، ولا يعيد إلا رسائل ثابتة، ولا يحفظ شيئًا. وهو محدود لكل مستخدم (5 في الدقيقة و20 في الساعة)، ولكل عنوان عميل (20 في الدقيقة)، وبمحاولة واحدة جارية لكل مستخدم؛ وتُحفظ هذه العدّادات لكل عملية خادم، فتتضاعف مع تعدد النسخ. وتُبقي صفحة الإعدادات زر اختبار الاتصال معطّلًا، لكن نقطة النهاية نفسها متاحة لأي مستخدم مسجّل الدخول.