list everything left open for the demo that must be closed before real users

docs/security-before-production.md records, item by item, what is exposed
on purpose while this is an internship demo: the committed token-signing key
still in use on the server, seven fixed-code demo logins, demo users in the
production database, a public /api/health that reports internal state, a
public API schema, a public leaderboard with children's names, /admin on the
public domain, the server answering on port 80 around ArvanCloud, per-worker
rate limits without Redis, guest login by deviceId alone, unhashed refresh
tokens and OTP codes, AI cost exposure through throwaway guests, hearts not
enforced, SQLite without off-box backups, and shared root access to the box.
Each has its risk, why it was left, the fix and how to verify it; the
exposure checks were run against the live server on 2026-09-25.

CLAUDE.md, README and docs/operations.md point to it so no agent mistakes
this deployment for production-ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:32:36 +03:30
parent 66066cbe23
commit 74aa9aa8ca
4 changed files with 237 additions and 0 deletions

View File

@@ -22,6 +22,15 @@ at checkout, `select_for_update` guards that SQLite silently ignores).
race-free / the right architecture"; write "guards X; not tested under Y" instead. race-free / the right architecture"; write "guards X; not tested under Y" instead.
Record uncertainty plainly so the next reader re-checks rather than trusts. Record uncertainty plainly so the next reader re-checks rather than trusts.
## Before any real user: read `docs/security-before-production.md`
The server is deliberately left in demo mode for the internship presentation: the token
signing key is the one committed in this repo, seven fixed-code demo logins exist, demo
users sit in the production DB, and `/api/health`, the API schema, the leaderboard and
`/admin/` are all public. Each is listed there with its risk, why it was left, and the
fix. **Do not treat this deployment as production-ready until every item there is closed**,
and record the date and commit of each fix in that file rather than deleting the item.
## Commands ## Commands
```bash ```bash

View File

@@ -119,6 +119,7 @@ curl https://api.math.tadrisino2.ir/api/health
| سند | موضوع | | سند | موضوع |
|---|---| |---|---|
| [`docs/operations.md`](docs/operations.md) | دیپلوی، متغیرهای محیطی، سلامت، بکاپ، ورودِ توسعه‌دهنده‌ها | | [`docs/operations.md`](docs/operations.md) | دیپلوی، متغیرهای محیطی، سلامت، بکاپ، ورودِ توسعه‌دهنده‌ها |
| [`docs/security-before-production.md`](docs/security-before-production.md) | **پیش از کاربرِ واقعی:** هرچه برای دمو عمداً باز مانده، خطرش و راهِ بستنش |
| [`docs/content-authoring.md`](docs/content-authoring.md) | افزودن و ویرایشِ محتوا از پنلِ ادمین، ساختِ انبوهِ سوال | | [`docs/content-authoring.md`](docs/content-authoring.md) | افزودن و ویرایشِ محتوا از پنلِ ادمین، ساختِ انبوهِ سوال |
| [`docs/architecture.md`](docs/architecture.md) | تصمیم‌های معماری و دلیلشان | | [`docs/architecture.md`](docs/architecture.md) | تصمیم‌های معماری و دلیلشان |
| [`docs/frontend/README.md`](docs/frontend/README.md) | راهنمای اتصالِ فرانت‌اند + کلاینتِ تایپ‌دار | | [`docs/frontend/README.md`](docs/frontend/README.md) | راهنمای اتصالِ فرانت‌اند + کلاینتِ تایپ‌دار |

View File

@@ -230,3 +230,10 @@ sudo systemctl restart gunicorn # مالِ ما نیست — اپِ
هر خطای پیش‌بینی‌نشده همیشه پاسخِ JSON برمی‌گرداند — نه صفحه‌ی HTML پیش‌فرضِ هر خطای پیش‌بینی‌نشده همیشه پاسخِ JSON برمی‌گرداند — نه صفحه‌ی HTML پیش‌فرضِ
جنگو — و کامل با traceback لاگ می‌شود (`config/exceptions.py`). جنگو — و کامل با traceback لاگ می‌شود (`config/exceptions.py`).
---
> **پیش از آمدنِ کاربرِ واقعی** فهرستِ [`security-before-production.md`](security-before-production.md)
> را کامل ببندید: کلیدِ امضا، حساب‌های دمو، جزئیاتِ عمومیِ `/api/health`، مستنداتِ عمومیِ API،
> جدولِ رتبه‌بندیِ عمومی، پنلِ ادمین، پورتِ ۸۰ و دسترسیِ سرور.

View File

@@ -0,0 +1,220 @@
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز مانده‌اند
**تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه `66066cb`.**
این پروژه تا این تاریخ در حالتِ **دمو و ارائه‌ی کارآموزی** است، نه محصولِ نهایی. چند چیز
عمداً باز گذاشته شده تا توسعه‌دهنده‌ها، فرانت‌کار و ارائه کار کنند. هر کدام برای یک سرورِ
عمومی با کاربرِ واقعی (کودک، پرداختِ واقعی) **خطر است** و باید پیش از آمدنِ اولین کاربرِ
واقعی بسته شود.
**برای agentها و توسعه‌دهنده‌های بعدی:** این فهرست ادعاست، نه ضمانت؛ ممکن است از این
تاریخ به بعد چیزی عوض شده باشد. پیش از بستنِ هر مورد، وضعیتِ فعلی‌اش را با خودِ سرور یا
کد بسنجید (ستونِ «چطور بسنجید»). بعد از بستنِ هر مورد، همین‌جا تاریخ و کامیتِ رفعش را
بنویسید؛ مورد را پاک نکنید.
هر آزمونِ «از بیرون» با `curl` بدونِ توکن انجام شده و در تاریخِ بالا تأیید شده است.
---
## خلاصه، به ترتیبِ خطر
| # | مورد | خطر | کارِ کیست |
|---|---|---|---|
| ۱ | کلیدِ امضای توکن روی سرور همان رشته‌ی داخلِ ریپو است | بحرانی — جعلِ ورود به‌جای هر کاربر | کد آماده؛ اعمال روی سرور |
| ۲ | ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن | بالا | `.env` سرور |
| ۳ | داده‌ی دمو و حسابِ مهمانِ عمومیِ `frontend-test-device` | بالا | دیتابیسِ سرور |
| ۴ | `/api/health` جزئیاتِ داخلی را به همه نشان می‌دهد | متوسط | کد |
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومی‌اند | متوسط | کد |
| ۶ | جدولِ رتبه‌بندی نام و شناسه‌ی کودکان را بدونِ ورود نشان می‌دهد | متوسط (حریمِ خصوصیِ کودک) | کد |
| ۷ | پنلِ `/admin/` روی دامنه‌ی عمومی | متوسط | nginx + کد |
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب می‌دهد | متوسط | ادمینِ سرور |
| ۹ | کشِ مشترک (Redis) نیست؛ محدودیت‌ها per-worker‌اند | متوسط | `.env` سرور |
| ۱۰ | ورودِ مهمان فقط با `deviceId` است | کم (طراحی) | کد |
| ۱۱ | توکنِ refresh و کدِ OTP بی‌هش در دیتابیس | کم | کد |
| ۱۲ | هزینه‌ی دستیارِ AI با ساختِ حسابِ مهمان قابلِ بالا بردن است | کم (مالی) | کد |
| ۱۳ | محدودیتِ قلب در سرور خاموش است (`HEARTS_ENFORCED=0`) | کم (کسب‌وکار) | `.env` + فرانت |
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین | کم (داده) | زیرساخت |
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
| ۱۶ | فرانت توکن را در `localStorage` نگه می‌دارد | کم | فرانت |
---
## ۱. کلیدِ امضای توکن (`DJANGO_SECRET_KEY`) روی سرور
- **چیست:** توکنِ ورود فقط `user_id`ِ امضاشده با `django.core.signing` است
(`accounts/authentication.py`). امضا با `DJANGO_SECRET_KEY` انجام می‌شود. روی سرور این
مقدار هنوز همان fallbackِ `django-insecure-…` است که در `config/settings.py` کامیت شده
(آخرین تأیید: ۲۰۲۶-۰۹-۲۴، به‌صورتِ بولین).
- **خطر:** هر کس ریپو را ببیند (Gitea، zipِ تحویلی، هر کپی) می‌تواند برای **هر `user_id`**
توکنِ معتبر بسازد و به‌جای آن کاربر وارد شود؛ از جمله خریدِ اشتراک، دیدنِ گفتگوها و
داده‌ی خانواده.
- **چرا باز مانده:** عوض کردنش همه‌ی کاربرهای واردشده (توسعه‌دهنده‌ها، فرانت‌کار، حساب‌های
دمو در حالِ ارائه) را بیرون می‌اندازد. تصمیمِ صاحبِ پروژه: بعد از ارائه.
- **رفع:** `DJANGO_SECRET_KEY` را از `.env` **و** از یونیتِ systemd حذف کنید تا
`config/settings.py` یک کلیدِ تصادفی بسازد و در `.django_secret_key` (مجوز `0600`) نگه
دارد. سرویس را ری‌استارت کنید. همه یک بار دوباره وارد می‌شوند.
- **چطور بسنجید:** `manage.py check --deploy` نباید `security.W009` بدهد؛ روی سرور
`venv/bin/python -c "from django.conf import settings; print(settings.SECRET_KEY.startswith('django-insecure'))"`
باید `False` چاپ کند (خودِ کلید را چاپ نکنید).
## ۲. حساب‌های دمو با کدِ ورودِ ثابت (`TEST_LOGIN_PHONES`)
- **چیست:** ۷ شماره‌ی `09120000101` تا `09120000107` با کدِ ثابت در `.env` سرور. شماره‌ها
در ریپو (`docs/operations.md`، `CLAUDE.md`) آمده‌اند؛ کدها فقط در راهنمای ارائه‌ی صاحبِ
پروژه‌اند.
- **خطر:** کسی که کد را از راهنما، اسکرین‌شات یا ارائه ببیند وارد آن حساب‌ها می‌شود. این
کدها با `OTP_MAX_FAILED_ATTEMPTS` باطل نمی‌شوند (ردیفِ `OTPCode` ندارند)؛ فقط سقفِ
per-workerِ تلاشِ ورود جلویشان است.
- **رفع:** `TEST_LOGIN_PHONES` را از `.env` سرور پاک کنید و ری‌استارت کنید.
- **چطور بسنجید:** `/api/health` دیگر هشدارِ «شماره‌های تستِ ورود با کدِ ثابت فعال‌اند» ندارد.
## ۳. داده‌ی دمو و مهمانِ عمومی
- **چیست:** کاربرهای ۶ (`deviceId = frontend-test-device`) و ۱۰ تا ۱۵، با درس، گفتگو،
تیکت، خانواده و زیرمجموعه، روی دیتابیسِ پروداکشن ساخته شده‌اند. `frontend-test-device`
در ریپو نوشته شده؛ هر کس `POST /api/auth/session` با همین `deviceId` بزند، توکنِ همان
حساب را می‌گیرد.
- **خطر:** داده‌ی ساختگی در آمار و جدولِ رتبه‌بندیِ واقعی؛ یک حسابِ عمومی که هر کس
می‌تواند با آن کار کند (ثبتِ تیکت، گفتگو با AI به هزینه‌ی ما).
- **رفع:** پیش از کاربرِ واقعی پاک شوند. `purge_demo_data` این‌ها را **نمی‌گیرد** (از مسیرِ
واقعیِ viewها ساخته شده‌اند)؛ باید با شناسه پاک شوند، پیش از آن بکاپ.
- **چطور بسنجید:** `POST /api/auth/session` با `frontend-test-device` باید `isNewUser: true` بدهد.
## ۴. `/api/health` عمومی و پرجزئیات
- **چیست:** `config/health.py`، `AllowAny`. بدونِ توکن برمی‌گرداند: `debug`، `warnings`
(مثلاً «شماره‌های تست فعال‌اند»، «محدودیتِ نرخ بینِ workerها مشترک نیست»)، `integrations`
(کدام سرویس‌ها پیکربندی‌اند، وضعیتِ اعتبارِ AI، نامِ مدل)، `traffic` (تعدادِ کاربر، فعالِ
۲۴ ساعتِ اخیر، زمانِ آخرین فعالیت)، `allowedOrigins`، `clientIp`، شمارِ محتوا و مهاجرت‌ها.
- **خطر:** نقشه‌ی حمله: مهاجم می‌فهمد محدودیت‌ها سه‌برابر دورزدنی‌اند، حسابِ کدِ ثابت
هست، اعتبارِ AI کی تمام شده، و ترافیکِ واقعی چقدر است.
- **چرا باز مانده:** صاحبِ پروژه و agentها بدونِ دسترسیِ SSH از همین صفحه سلامتِ سرور را
می‌بینند.
- **رفع:** پاسخِ عمومی فقط `{"status": "ok"|"degraded"}`؛ بقیه پشتِ هدرِ
`X-Internal-Key` (`INTERNAL_SERVICE_KEY`) یا فقط برای staff.
- **چطور بسنجید:** `curl https://api.math.tadrisino2.ir/api/health` بدونِ هدر نباید
`warnings` و `traffic` داشته باشد.
## ۵. مستنداتِ API عمومی
- **چیست:** `/api/docs/` (Swagger)، `/api/redoc/` و `/api/schema/` بدونِ ورود باز‌اند
(تأیید: HTTP 200). schema همه‌ی endpointها، پارامترها، کدهای خطا و هدرهای داخلی
(`X-Internal-Key`) را فهرست می‌کند.
- **خطر:** شناساییِ کاملِ سطحِ حمله بدونِ زحمت.
- **چرا باز مانده:** قراردادِ مشترک با فرانت همین‌جاست؛ فرانت‌کار و ارائه به آن نیاز دارند.
- **رفع:** در پروداکشن `SERVE_PERMISSIONS` در `SPECTACULAR_SETTINGS` را به
`IsAdminUser` (staffِ جنگو) محدود کنید، یا مسیر را در nginx به IPهای تیم ببندید.
- **چطور بسنجید:** `curl -o /dev/null -w "%{http_code}" …/api/schema/` باید ۴۰۱/۴۰۳ بدهد.
## ۶. جدولِ رتبه‌بندیِ عمومی — حریمِ خصوصیِ کودک
- **چیست:** `GET /api/leaderboard` بدونِ توکن (تأیید: HTTP 200) برای هر ردیف `rank`،
`userId`، `name` و `xpInPeriod` می‌دهد.
- **خطر:** نامِ کودکان و شناسه‌ی داخلی‌شان برای همه‌ی اینترنت قابلِ جمع‌آوری است.
پاک‌سازیِ نام (حذفِ شماره و لینک، `clean_display_name`) فقط راهِ تماس را می‌گیرد، نه خودِ نام را.
- **رفع:** `IsAuthenticated`؛ حذفِ `userId` از پاسخ (جز برای ردیفِ خودِ کاربر)؛ نمایشِ
نامِ کوچک یا نامِ مستعار.
## ۷. پنلِ ادمین روی دامنه‌ی عمومی
- **چیست:** `/admin/` روی `api.math.tadrisino2.ir`، با ورودِ نام‌کاربری/رمزِ جنگو. از
۲۰۲۶-۰۹-۲۵ سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (`AdminLoginThrottleMiddleware`)،
ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعل‌پذیر است (مورد ۸)، و ۲FA ندارد.
- **خطر:** ادمین همه‌ی محتوا، پلن‌ها، کدهای تخفیف، سفارش‌ها و کاربرها را کنترل می‌کند.
- **رفع:** محدود کردنِ `/admin/` در nginx به IPهای تیم یا VPN؛ تغییرِ مسیرِ پیش‌فرض؛ ۲FA؛
رمزهای قوی و حسابِ staffِ جدا برای هر نفر.
## ۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud)
- **چیست:** `http://78.38.35.249/api/health` با `Host: api.math.tadrisino2.ir` پاسخ می‌دهد
(تأیید: HTTP 200). `NUM_PROXIES=2` فرض می‌کند همیشه دو پروکسی جلوی ماست.
- **خطر:** کسی که IP را بداند `X-Forwarded-For` را جعل می‌کند و محدودیت‌های IP-محور
(مهمان، OTP، ادمین) را دور می‌زند.
- **رفع (ادمینِ سرور):** پورتِ ۸۰/۴۴۳ فقط برای رنج‌های ArvanCloud، یا `set_real_ip_from` +
`real_ip_header` در nginx.
## ۹. نبودِ کشِ مشترک (`CACHE_URL`)
- **چیست:** بدونِ `CACHE_URL` هر worker کشِ `LocMemCache` خودش را دارد (۳ worker).
- **خطر:** همه‌ی محدودیت‌های مبتنی بر کش تا سه برابر شل‌ترند: throttleهای DRF، cooldown و
سقفِ درخواستِ OTP، سقفِ تلاشِ ورودِ OTP به‌ازای شماره، سقفِ ورودِ ادمین؛ و باطل‌شدنِ کش
بعد از تغییرِ ادمین فقط در یک worker. (سقفِ حدسِ هر کدِ OTP در دیتابیس است و متأثر نیست.)
- **رفع:** `CACHE_URL=redis://127.0.0.1:6379/1` (db0 مالِ اپِ دیگری است) و ری‌استارت.
- **چطور بسنجید:** `/api/health` → `checks.cache.backend` نباید `LocMemCache` باشد.
## ۱۰. ورودِ مهمان فقط با `deviceId`
- **چیست:** `POST /api/auth/session` با هر `deviceId` یا حسابِ مهمانِ همان دستگاه را
برمی‌گرداند یا می‌سازد (`accounts/views.py::GuestSessionView`). هیچ رازِ دیگری در کار نیست.
- **خطر:** هر کس `deviceId`ِ یک مهمان را بداند (از `localStorage`، لاگ، اسکرین‌شات) وارد
حسابِ او می‌شود. بعد از ثبت‌نام با شماره این راه بسته می‌شود (مهمان دیگر مهمان نیست).
- **رفع:** برای مهمان هم یک رازِ تصادفیِ سمتِ سرور صادر شود و ورودِ دوباره با همان راز باشد،
نه با `deviceId`ِ خام.
## ۱۱. داده‌ی حساس بی‌هش در دیتابیس
- **چیست:** `Session.refresh_token` و `OTPCode.code` به‌صورتِ متنِ ساده ذخیره می‌شوند
(`accounts/models.py`) و در پنلِ ادمین (`Session`, `OTPCode`) دیده می‌شوند.
- **خطر:** هر کس بکاپِ دیتابیس یا پنلِ ادمین را ببیند نشستِ هر کاربر را می‌رباید.
بکاپ‌ها در `.deploy-backups/` کنارِ خودِ پروژه روی یک ماشینِ مشترک‌اند.
- **رفع:** ذخیره‌ی هشِ SHA-256 و مقایسه با هش؛ حذفِ این دو فیلد از نمایشِ ادمین.
## ۱۲. هزینه‌ی دستیارِ AI
- **چیست:** سقفِ روزانه ۱۵ سؤال **برای هر کاربر** (`DAILY_AI_QUOTA`) و ۲۰ در ساعت؛ مهمان‌ها
بر اساسِ IP محدود می‌شوند ولی هر IP ساعتی ۲۰ حسابِ مهمانِ تازه می‌سازد (`guest_session`)،
و هر حسابِ تازه ۱۵ سؤالِ تازه دارد. طولِ سؤال از ۲۰۲۶-۰۹-۲۵ حداکثر ۲۰۰۰ نویسه است.
- **خطر:** با چند IP، اعتبارِ GapGPT در چند ساعت تمام می‌شود (اعتبار یک بار در ۲۰۲۶-۰۹-۲۵
به صفر رسید، البته به دلیلِ دیگر).
- **رفع:** سقفِ سراسریِ روزانه برای کلِ اپ؛ دستیار فقط برای کاربرِ واردشده با شماره یا
با سقفِ بسیار کمتر برای مهمان؛ هشدار وقتی `aiProvider.state` در `/api/health` برابرِ
`quota_exhausted` شد.
## ۱۳. محدودیتِ قلب خاموش است
- **چیست:** با صفر قلب، سرور همچنان جواب را می‌پذیرد، مگر `HEARTS_ENFORCED=1`
(`content/views.py::QuestionAnswerView`). «قلبِ نامحدود» تنها مزیتِ اشتراک است.
- **خطر:** مزیتِ پولی فقط در ظاهرِ اپ اعمال می‌شود؛ با صدا زدنِ مستقیمِ API دور زده می‌شود.
- **چرا باز مانده:** فرانت برای ۴۰۳ِ `no_hearts` هنوز صفحه ندارد؛ روشن کردنش دانش‌آموز را به
صفحه‌ی خطا می‌برد. رفتار با صفر قلب هم تصمیمِ محصولیِ باز است.
- **رفع:** بعد از صفحه‌ی «قلب‌ها تمام شد» در فرانت، `HEARTS_ENFORCED=1` در `.env`.
## ۱۴. دیتابیس و بکاپ
- **چیست:** SQLite در `~/mathapp-v2/db.sqlite3`، روی دیسکی که بینِ چند سرویس مشترک است و
~۸۳٪ پر است. `transaction_mode: IMMEDIATE` هم‌زمانی را درست ولی سریالی می‌کند. بکاپ دستی
است (`deploy.sh` پیش از `migrate` بکاپ نمی‌گیرد) و در همان پوشه می‌ماند.
- **خطر:** از دست رفتنِ کلِ داده با یک خطای دیسک یا یک دستورِ اشتباه؛ پُر شدنِ دیسک
سرویس‌های دیگر (Gitea، ایمیل) را هم می‌خواباند.
- **رفع:** مهاجرت به Postgres (روی همان ماشین نصب است)، بکاپِ خودکار پیش از هر `migrate`، و
نگه‌داریِ بکاپ بیرون از ماشین.
## ۱۵. دسترسیِ سرور
- **چیست:** ورود با رمز به حسابِ مشترکِ `administrator` با `sudo (ALL : ALL) ALL` و عضوِ
`docker` و `lxd`، روی ماشینی که Gitea و ایمیلِ شرکت هم رویش است. رمز یک بار در گفتگو با
agent گذاشته شده و روی دسکتاپِ صاحبِ پروژه در یک فایلِ متنی نگه داشته می‌شود.
- **خطر:** هر نشتِ رمز یعنی دسترسیِ root به کلِ ماشینِ شرکت، نه فقط این پروژه.
- **رفع (ادمینِ سرور):** کلیدِ SSH به‌جای رمز، تعویضِ رمز، حسابِ جدا برای این پروژه با
دسترسیِ فقط به `~/mathapp-v2` و یک خطِ `NOPASSWD` فقط برای
`systemctl restart zareian-mathapp`؛ پاک کردنِ فایلِ رمز از دسکتاپ (Keychain جایگزین است).
## ۱۶. نگه‌داریِ توکن در فرانت
- **چیست:** کلاینتِ فرانت (`src/api/index.ts` در مخزنِ فرانت) توکن و refresh را در
`localStorage` نگه می‌دارد.
- **خطر:** هر XSS در سایت هر دو را می‌خواند.
- **رفع:** CSPِ سخت‌گیرانه روی `math.tadrisino2.ir`؛ در بلندمدت refresh در کوکیِ `HttpOnly`.
---
## چیزهایی که بسته شده‌اند (برای اینکه دوباره باز نشوند)
در بازبینیِ ۲۰۲۶-۰۹-۲۵ (کامیتِ `42783f9`) بسته شدند؛ جزئیات در `docs/architecture.md`:
پلنِ هر خرید روی سفارش، دست نخوردنِ اشتراکِ فعال هنگامِ خرید، یک کدِ تخفیف پیش از درگاه،
حذفِ `/api/payment/webhook`، امتیاز فقط از جوابِ ثبت‌شده، OTPِ ۶ رقمی با ۵ حدس، خروج از حساب،
حفظِ مهمان هنگامِ ثبت‌نام، `select_for_update` مؤثر با `IMMEDIATE`، سقفِ ورودِ ادمین، سقفِ
طولِ سؤالِ AI، مقایسه‌ی زمان‌ثابتِ `X-Internal-Key`.
هر تغییری در این مسیرها باید تست‌های `payment/tests_audit_fixes.py` و
`accounts/tests_audit_fixes.py` را سبز نگه دارد.