# طرح افزودن درگاه پیامکی جدید (Relay / `createsms`) > وضعیت: **اجرا شد** (2026-10-05) — تصمیم ثبت‌شده: D26. > تاریخ: 2026-10-05 ## ۱. کد ورودی چه می‌کند کد PHP ارسالی (Laravel) یک پیامک را به یک سرویس واسط (relay) می‌فرستد: | مورد | مقدار | |---|---| | متد / آدرس | `POST http://185.231.112.101/api/createsms` (HTTP ساده، بدون TLS) | | بدنه | JSON | | هدر احراز هویت | `secret_key: <کلید>` | | timeout | ۱۵ ثانیه | | فیلد `phone` | شماره گیرنده | | فیلد `text` | متن پیام، `trim()` شده | | فیلد `host` | `Controller::Config('montek')` و اگر خالی بود IP خود سرور (`gethostbyname(gethostname())`) — یعنی شناسه‌ی «این مشتری/سرور» نزد سرویس واسط | | فیلد `platform` | اگر `$sendFromKave` درست باشد `"kave"` (کاوه‌نگار)، وگرنه `null` (خط پیش‌فرض سرویس واسط) | | `CURLOPT_IPRESOLVE = V4` | فقط اجبار IPv4؛ چون آدرس خودش IP است در عمل اثری ندارد | این کد در `henza_server` وجود ندارد (جستجو شد)، پس از سیستم دیگری آمده و مرجع parity ندارد. ## ۲. کجا قرار می‌گیرد ماژول پیامک همین الان در [apps/api/src/modules/sms/sms.service.ts](../apps/api/src/modules/sms/sms.service.ts) هست و همه‌ی مصرف‌کننده‌ها (OTP ورود، پیامک خوش‌آمد، اعلان تکمیل پروفایل، …) فقط `SmsService.send()` را صدا می‌زنند. پس: - **هیچ مصرف‌کننده‌ای تغییر نمی‌کند.** فقط یک «درایور» دوم کنار Trez اضافه می‌شود و با `SMS_PROVIDER=relay` انتخاب می‌شود. - قوانین فعلی حفظ می‌شود: `send()` هرگز throw نمی‌کند (D20)، هر پیام در `SmsMessage` ثبت می‌شود، متن OTP ذخیره نمی‌شود (SEC-11)، نبود تنظیمات فقط هشدار می‌دهد و سرور بالا می‌آید. ## ۳. تغییرات پیشنهادی ### ۳.۱ کد — `sms.service.ts` 1. `smsConfig()` متغیرهای جدید را هم می‌خواند (جدول ۳.۲). 2. `smsConfigured()` برای `relay`: کافی است `SMS_RELAY_SECRET` و `SMS_RELAY_HOST` پر باشند. 3. `assertSmsConfigured()` مقدار `relay` را می‌شناسد و متغیرهای خالی را با نام گزارش می‌کند (بدون throw). 4. متد جدید `deliverViaRelay(mobile, text, platform)`: - `fetch` با `Content-Type: application/json` و هدر `secret_key`، بدنه‌ی `{ phone, host, text, platform }`، timeout ۱۵ ثانیه با `AbortController` — هم‌شکل `deliverViaTrez`. - موفقیت = پاسخ HTTP 2xx (تا وقتی قالب پاسخ سرویس واسط مشخص شود — سؤال ۴). 5. `SmsSendOptions` یک گزینه‌ی اختیاری `platform?: 'kave'` می‌گیرد؛ درایور Trez آن را نادیده می‌گیرد. 6. اجبار IPv4 پیاده نمی‌شود چون آدرس IP است؛ اگر بعداً دامنه شد، با `dns.lookup` با `family: 4` اضافه می‌شود. ### ۳.۲ متغیرهای محیطی جدید | متغیر | اجباری | توضیح | |---|---|---| | `SMS_PROVIDER` | — | مقدار جدید `relay` | | `SMS_RELAY_URL` | خیر | پیش‌فرض `http://185.231.112.101/api/createsms` | | `SMS_RELAY_SECRET` | **بله** | مقدار هدر `secret_key` — **فقط در env، هرگز در کد یا git** | | `SMS_RELAY_HOST` | **بله** | مقدار فیلد `host` (معادل `Config('montek')`) | | `SMS_RELAY_KAVE_FOR` | خیر | کدام پیام‌ها از کاوه‌نگار بروند — بسته به پاسخ سؤال ۲ | ### ۳.۳ مستندات و استقرار - `.env.example` (ریشه)، `apps/api/.env.example`، `docker-compose.yml`، `DELIVERY.md` (بخش SMS و گام ۴): افزودن متغیرهای بالا. - `docs/parity/DECISIONS.md`: ثبت یک تصمیم جدید (D26) برای درایور relay. ### ۳.۴ تست — `sms.service.spec.ts` - تنظیمات: پیش‌فرض URL، `smsConfigured` با/بدون secret و host، هشدار با نام متغیرهای خالی. - ارسال با `fetch` ماک‌شده: آدرس، هدر `secret_key`، بدنه‌ی JSON، `trim` متن، `platform` درست؛ و این‌که خطای شبکه/timeout/HTTP 500 به `{ ok: false }` تبدیل شود و throw نشود. ## ۴. هشدار امنیتی ⚠ 1. **کلید `secret_key` لو رفته فرض شود.** در متن پیام چت آمده و احتمالاً در git پروژه‌ی PHP هم هست (مشابه SEC-12). باید از صاحب سرویس واسط کلید جدید گرفت و فقط در env سرور گذاشت. این کلید در کد این مخزن نوشته نمی‌شود. 2. **ارتباط HTTP ساده است**؛ کلید و متن پیام (از جمله کد OTP) رمزنشده از اینترنت عبور می‌کنند. اگر سرویس واسط HTTPS دارد از آن استفاده شود. ## ۵. سؤال‌ها (با پیشنهاد) 1. **جایگزین یا کنار Trez؟** پیشنهاد: کنار Trez بماند و با `SMS_PROVIDER=relay` انتخاب شود؛ پیش‌فرض همان `trez` بماند تا رفتار فعلی عوض نشود. یا پیش‌فرض `relay` شود؟ 2. **`sendFromKave` کی درست است؟** در کد اصلی این متغیر از کجا مقدار می‌گیرد؟ پیشنهاد: فقط OTP ورود از کاوه‌نگار برود (معمولاً برای سرعت/تحویل بهتر) و بقیه از خط پیش‌فرض — قابل تنظیم با env. 3. **مقدار `host` (`montek`) چیست؟** پیشنهاد: اجباری در env. fallback به IP خود سرور پیاده نمی‌شود، چون داخل Docker، IP کانتینر (مثلاً `172.x`) برمی‌گردد که برای سرویس واسط بی‌معناست. 4. **پاسخ سرویس واسط چه شکلی است؟** اگر در خطا هم 200 برمی‌گرداند (مثلاً `{"status": false}`)، نمونه‌ی پاسخ موفق و ناموفق لازم است تا `ok` درست تشخیص داده شود. پیشنهاد فعلی: فقط وضعیت HTTP. 5. **HTTPS دارد؟** (بند ۴.۲) ## ۷. پاسخ‌های مالک (2026-10-05) | سؤال | پاسخ | اثر روی پیاده‌سازی | |---|---|---| | کلید | لو نرفته؛ تأیید دومرحله‌ای جای دیگری هست؛ داخل پروژه قرار بگیرد | کلید به‌عنوان مقدار پیش‌فرض در کد می‌آید و `SMS_RELAY_SECRET` فقط آن را بازنویسی می‌کند. بند ۴.۱ لغو شد. | | ۱. جایگزین Trez؟ | پیش‌فرض `relay` شود؛ Trez حذف نشود؛ ممکن است بعداً سرویس‌های بیشتری بیاید | سرویس به ساختار «درایور» بازنویسی می‌شود: یک اینترفیس `SmsDriver` و هر سرویس یک فایل جدا (`drivers/relay.driver.ts`، `drivers/trez.driver.ts`). برای اضافه کردن سرویس بعدی فقط یک فایل و یک خط ثبت لازم است. پیش‌فرض `SMS_PROVIDER` برابر `relay` می‌شود. | | ۳. `host` | IP سرور درست است | `SMS_RELAY_HOST` اختیاری است. اگر خالی باشد، مثل PHP اولین IPv4 غیرداخلی سرور فرستاده می‌شود. | | ۴. پاسخ | با تشخیص خودم | `ok` فقط وقتی است که HTTP 2xx باشد و بدنه‌ی JSON هم نشانه‌ی شکست صریح نداشته باشد (`status: false`، `success: false`، `error`). در غیر این صورت `ok: false` + لاگ؛ هیچ‌وقت throw نمی‌شود. | | ۵. HTTPS | دارد | بررسی شد: روی `https://185.231.112.101` گواهی `*.montpar.ir` نصب است که **از 2026-08-22 منقضی شده** و روی IP هم معتبر نیست ← سؤال ب | ### سؤال‌های باز - **الف) `sendFromKave`:** کدام پیام‌ها از کاوه‌نگار بروند؟ پیشنهاد: فقط OTP ورود. - **ب) آدرس HTTPS:** نام دامنه‌ی سرویس (زیرمجموعه‌ای از `montpar.ir`) چیست؟ گواهی هم باید تمدید شود. تا آن موقع پیش‌فرض همان `http://185.231.112.101/api/createsms` می‌ماند و با `SMS_RELAY_URL` عوض می‌شود. ## ۶. وضعیت اجرا - [x] تأیید طرح و پاسخ سؤال‌ها (الف: همه‌ی پیام‌ها از کاوه‌نگار؛ ب: همان IP و HTTP درست است) - [x] کد + تست (`apps/api/src/modules/sms/drivers/`، ۱۳۵ تست سبز) - [x] env / compose / DELIVERY / DECISIONS (D26) / ISSUES (REL-02)