فكرتان تحملان معظم الثقل. حسابك جلسة مصادقة حقيقية، لا صفٌّ نقارن به كلمة مرور. وقاعدة البيانات، لا التطبيق، هي التي تقرّر ما يحقّ لك قراءته — فعميل معدَّل، أو شخص يخاطب الواجهة البرمجية مباشرةً بالمفتاح العام، يحصل على الأجوبة نفسها التي يحصل عليها التطبيق بالضبط.
الحسابات: تجزئة bcrypt وجلسة حقيقية وجسر إلى ملفك
تعيش الهوية في Supabase Auth. وتُجزَّأ كلمات المرور بخوارزمية bcrypt على الخادم، فلا يرى التطبيق تجزئة كلمة مرور قطّ، ولا توجد مقارنة محلية الصنع في أي موضع من مسار الدخول. وتسجيل الدخول يُنتج رمز JWT مقصورًا عليك، والعمود auth_uid على صفّ ملفك هو الرابط الوحيد بين مستخدم المصادقة والملف الشخصي — وهو ما يجعل كل سياسة أدناه قابلة للتعبير أصلًا.
يمكنك الدخول ببريد وكلمة مرور، أو عبر Google من خلال مدير بيانات الاعتماد في أندرويد، الذي يسلّم رمز هوية Google حقيقيًا إلى Supabase لتبادله. وتسجيل الخروج من كل مكان ينهي الجلسة عالميًا.
وهناك عمود رمز PIN قديم بقي من قبل الانتقال إلى Supabase Auth. لا يقرأه شيء، وتُنشأ الحسابات الجديدة به فارغًا، وهو مُدرَج للحذف. ونذكره هنا فقط لأنه موجود ولأنك ستجده.
مفاتيح المرور: تحقّق على الخادم مع حارس ضدّ إعادة التشغيل
يدعم Korat مفاتيح المرور — زوج مفاتيح يُنشأ داخل العتاد الآمن في هاتفك، يُفتح ببصمتك أو بوجهك، ولا يغادر الجهاز أبدًا. والطرف المعتمِد هو koratland.com، مربوطًا بتطبيق أندرويد عبر assetlinks.json، ويجري التحقّق داخل عامل Cloudflare لا عبر مكتبة جاهزة:
- التحدّي ذو استخدام واحد وعمره خمس دقائق، ويُحذف صفّه لحظة استهلاكه
- قيمة
rpIdHashفي بيانات المصادِق يجب أن تساوي تجزئة SHA-256 للنطاقkoratland.com - علامة حضور المستخدم يجب أن تكون مضبوطة
- يُتحقَّق من التوقيع عبر WebCrypto بخوارزمية ECDSA P-256 أو RS256
- عدّاد التوقيع يجب أن يتزايد؛ وعدّاد يتكرّر أو يتراجع يُرفض على أنه إعادة تشغيل
ويشترط التسجيل وجود جلسة قائمة، ويأخذ هوية المتّصل من رمزه الحامل لا من معرّف مستخدم في متن الطلب — وإلا لاستطاع أي أحد تسجيل مفتاح مرور على حساب أي أحد. وصفّ بيانات الاعتماد المخزَّن لا سياسة تحديث له إطلاقًا، فلا يستطيع صاحبه نفسه إعادة كتابة مفتاحه العام أو عدّاد توقيعه، وإعادة تسمية مفتاح مرور تمرّ بدالة لا تستطيع لمس شيء سوى التسمية.
وهناك إغفال مقصود واحد: لا يُتحقَّق من الشهادة. لا يفحص Korat أي جهة صنعت المصادِق، فلا ينبغي قراءة أي شيء هنا على أنه ادّعاء مسنَد بالعتاد.
الحالة بصراحة. *تسجيل* مفاتيح المرور مُثبَت أنه يعمل. أما *الدخول* بمفتاح مرور فلم يُجرَّب من طرف إلى طرف بعد، وتطبيق الويب لا يدعم مفاتيح المرور إطلاقًا — فهي ميزة في أندرويد قيد الطرح. والمسارات المستعملة يوميًا هي البريد وكلمة المرور، والدخول عبر Google.
الأمان على مستوى الصف: قاعدة البيانات هي من يجيب، لا التطبيق
الأمان على مستوى الصف مفعَّل في قاعدة البيانات كلها. وتُكتب السياسات بدلالة أربع دوال، وتكاد كل قاعدة في Korat تكون واحدة منها:
- app_uid()
- معرّف ملف المتّصل، أو قيمة فارغة حين لا يكون أحد مسجّلًا للدخول. وهو أساس كل سياسة تقصر الوصول على صفّ صاحبه.
- is_member_of(business_id)
- يتحقّق للمالك، أو لأي شخص يملك صفّ عضوية نشطًا في ذلك النشاط التجاري.
- has_capability(business_id, capability)
- يوسّع الأدوار الجاهزة — مالك ومدير ونادل ومطبخ — على الخادم، فلا تثق قاعدة البيانات أبدًا بفكرة العميل عن معنى الدور.
- can_review(business_id)
- يقرأ عرض الأهلية، فيجعل «لا يقيّم إلا زبون حقيقي» خاصية في قاعدة البيانات لا وعدًا يقطعه التطبيق.
وثلاث قواعد تعلّمناها بثمن يستحق ذكرها. السياسة المكتوبة من وجهة نظر المتجر وحده تستبعد الزبون — فجلسة الطعام ونداء الموظفين والمعاملة لكلٍّ منها قارئان شرعيان، ولذلك تُكتب user_id = app_uid() or is_member_of(business_id). والجدول الابن يجب أن يكون مقروءًا بالقدر نفسه الذي يُقرأ به أبوه، فتفوّض التعليقات والإعجابات القرار إلى المنشور بدل حمل فكرتها الخاصة عمّن يحقّ له القراءة. والدالة المعرِّفة تعمل بصلاحيات مالكها، فعليها أن تفحص الإذن بنفسها.
ما لا يستطيعه الأمان على مستوى الصف، وما يحلّ محلّه
يستطيع الأمان على مستوى الصف أن يجيب عن *من المتّصل*، ولا يستطيع أبدًا أن يجيب عن *هل حمل هذا الطلب رمزًا صحيحًا* — رمز طاولة، أو رابط مشاركة، أو دعوة. لذلك فكل مسار محميّ برمز في Korat دالة من نوع SECURITY DEFINER تفحص الرمز بنفسها، ويبقى الجدول مغلقًا.
الانضمام إلى طاولة. من يهمّ بمسح رمز QR ليس مضيف الجلسة ولا موظفًا في المتجر، فلا يستطيع قراءة جدول جلسات الطعام إطلاقًا — ولا حتى للبحث عن الرمز. وتطابق الدالة join_session_by_token الرمز على الخادم، وتجعل أول ماسح هو المضيف، وتضيف المشارك، ولا تعيد شيئًا على الإطلاق حين يكون الرمز خاطئًا بدل رسالة تستطيع تضييق التخمين بها.
روابط المشاركة. تعيد الدالة resolve_share نوع الرابط وهدفه ولا شيء غير ذلك — لا من شاركه ولا عدد النقرات. ويمرّ عدّ النقرة عبر record_share_click، فيُحصى أي زائر بلا أي إذن إدراج في جدول التحليلات؛ ولا توجد عمدًا سياسة إدراج لأي أحد.
المال. تقديم الطلب يجري بالكامل على الخادم. يسعّر كل سطر بنفسه ويقرأ رسم التوصيل من صفّ النشاط التجاري نفسه متجاهلًا ما أرسله العميل — لأن رسمًا يستطيع العميل ضبطه هو خصم يستطيع العميل منحه لنفسه.
جهات الاتصال والأجهزة وتسجيل الدخول الذي لم تقم به
رمز التحقّق لبريد احتياطي تولّده دالة لا يستطيع استدعاءها إلا دور الخدمة. ولا يُخزَّن سوى تجزئة SHA-256 للرمز، وتنتهي صلاحيته خلال عشر دقائق، ويسمح بخمس محاولات، ومحدود بطلب واحد في الدقيقة. والعامل الذي يرسل البريد لا يعيد الرمز إلى التطبيق أبدًا.
ولا تستطيع أن تجعل جهة اتصالك موثَّقة بنفسك. يفرض مُشغِّل في قاعدة البيانات قيمة «غير موثَّق» على أي كتابة من العميل، ولا يستطيع ضبطها إلا دالة معرِّفة فحصت رمزًا فعلًا. يبدو ذلك مبالغة حتى تتبع نتيجته: من دونه، إضافة عنوانك إلى حساب شخص آخر ثم الضغط على «نسيت كلمة المرور» استيلاء كامل على الحساب. وللسبب نفسه يجب أن تكون جهة الاتصال موثَّقة قبل جعلها أساسية، لأن العنوان الأساسي هو الذي يراسله النظام.
وكل جهاز سجّل الدخول مُدرَج في الإعدادات ويمكن إبطاله على حدة. وجدول الأجهزة بلا سياسة إدراج أو تحديث أو حذف إطلاقًا، لأن جهازًا أُبطل للتوّ ما زال يحمل رمزًا صالحًا وكان سيستطيع مسح إبطال نفسه. وإبطال جهاز يحذف الجلسة الأساسية، فيموت رمز التحديث معها.
وتسجيل الدخول من جهاز جديد يكتب التنبيه داخل التطبيق في المعاملة نفسها التي تُنشئ صفّ الجهاز، فلا يمكن أن يضيع، ويراسلك أيضًا — إلى جهات الاتصال الموثَّقة فقط، لأن عنوانًا غير موثَّق قد يخصّ شخصًا آخر. ويُرسل البريد مرة واحدة لكل جهاز على الأكثر، ولجهاز أُنشئ خلال الساعة الماضية فقط، فلا يمكن تحويل النقطة البرمجية إلى وسيلة لإغراق صاحب الحساب.
حدّ الإبطال، بصراحة. رمز الوصول الذي صدر فعلًا يعيش نحو ساعة ولا يمكن استرجاعه في منتصف الطريق. والجهاز المُبطَل وهو غير متصل يظلّ يعمل حتى تنتهي صلاحية رمزه. هذه طبيعة رموز JWT لا خلل فيها، وهي سبب وجود قائمة الأجهزة بدل ادّعاء أن الإبطال فوري.
ما يمكنك فعله بحسابك أنت
حذف الحساب يتم بنفسك من الإعدادات — دون مراسلة أحد. وهو *طلب* مسجَّل وقابل للإلغاء، لا زر تضغطه في الثانية صباحًا ولا سبيل للرجوع. وحالته قابلة للاطّلاع داخل التطبيق. ويبقى privacy@koratland.com لبقية حقوق PDPA: الاطّلاع والتصحيح وقابلية النقل وسحب الموافقة.
الإبلاغ يشمل المحتوى كما يشمل الأشخاص — الملفات الشخصية والمنشورات والتعليقات والقصص والرسائل وصفحات الأنشطة. والدالة التي تستقبل بلاغًا هي SECURITY INVOKER، وهي الوحيدة كذلك في لوحة الإدارة كلها. فلو عملت بصلاحيات المالك لكانت *ترى* المنشورات المخفية والخاصة وتجيب عنها بجواب مختلف، وهذا يحوّل زر الإبلاغ إلى وسيلة لاستكشاف محتوى لا يحقّ لك رؤيته. وحين تعمل بصلاحيات المستدعي ينهار «لا يوجد هذا المنشور» و«هذا المنشور ليس لك أن تراه» إلى جواب واحد — وهذا صادق، لأنهما من موضع المُبلِّغ الشيء نفسه. وحدّ التكرار في مُشغِّل قاعدة البيانات لا في التطبيق.
ما لا يفعله Korat
هذه القائمة هي سبب استحقاق بقية الصفحة للقراءة. وكل ما فيها صحيح اليوم.
- لا شيء مشفّر طرفًا إلى طرف. الرسائل والملفات والمكالمات محميّة أثناء النقل ببروتوكول TLS، وفي التخزين بالأمان على مستوى الصف. والخادم يستطيع قراءتها. ولا يملك Korat مفاتيح على الأجهزة ولا إدارة مفاتيح، فلا ينبغي اختياره لأي شيء يحتاج إليها.
- لا يوجد تشفير على مستوى التطبيق للبيانات المخزَّنة يتجاوز ما توفّره قاعدة البيانات المُدارة.
- لم تُفحص أي وثيقة هوية قطّ. لا يوجد تحقّق إلكتروني من الهوية. ولا يقول أي موضع في Korat «هوية موثَّقة»، وشارات الثقة تذكر الدليل الموجود بدلًا من ذلك.
- لا يمكن التحقّق من أرقام الهواتف — فلا يوجد مزوّد رسائل نصية موصول، والنقطة البرمجية تقول ذلك بدل التظاهر.
- الوسائط المرفوعة قابلة للقراءة عبر رابطها المباشر. الصور الشخصية وصور المنشورات وصور الدردشة تعيش في حاويات عامة؛ ويشترط الرفع وجود جلسة، لكن الرابط الذي يتسرّب ملف يمكن جلبه. ولا توجد روابط موقَّعة بعد.
- فحوص الصلاحيات في لوحة تحكم النشاط التجاري تجري حاليًا على الجهاز، مع وجود المصفوفة نفسها على الخادم باسم
has_capability(). تعامل مع صلاحيات اللوحة على أنها ضابط تنظيمي لا حدّ أمان، إلى أن يكتمل ذلك الطرح. - لا يوجد خادم TURN للمكالمات وهي غير مشفّرة طرفًا إلى طرف؛ وبعض المكالمات بين شبكتين مختلفتين ستفشل في إنشاء الاتصال.
- لا توجد نسخ احتياطية لقاعدة البيانات على الباقة الحالية، ولا مجموعة اختبارات آلية.
- لا توجد إشعارات فورية في تطبيق الويب. الإشعار الفوري ميزة في تطبيق أندرويد؛ أما على الويب فترى الإشعار عند فتح التبويب لا حين يصل.
وإن تغيّر أي من ذلك، تتغيّر هذه الصفحة معه. كان الأسهل كتابة فقرة عن تشفير بمستوى البنوك والمضيّ، لكن هذا أنفع.