mn-sms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The mn-sms contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,249 @@
1
- # Temporary Holding Version
1
+ # mn-sms
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** · [Монгол](README.mn.md)
4
+
5
+ Send SMS through the Mongolian mobile operators' own gateways — **Unitel**, **Mobicom**,
6
+ **Skytel** and **G-Mobile** — from one client. You give it a number and a message; it works
7
+ out which operator owns the number and calls that operator's gateway.
8
+
9
+ - Zero dependencies. Node 18+ (uses the built-in `fetch`). ESM and CommonJS, typed.
10
+ - Routes by the official number ranges, not by guessing from the first two digits.
11
+ - Reads each gateway's real verdict: three of the four answer HTTP `200` for a refused send.
12
+ - Never puts a credential in a result, an error message or a log line.
13
+
14
+ > **This is not an SMS reseller.** There is no shared account behind this package. You need
15
+ > your own contract with each operator you want to send through — see
16
+ > [What you need from the operators](#what-you-need-from-the-operators).
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install mn-sms
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { createSmsClient } from "mn-sms";
28
+
29
+ const sms = createSmsClient({
30
+ // Configure only the operators you have an account with.
31
+ unitel: { apiKey: process.env.UNITEL_API_KEY! },
32
+ skytel: { id: process.env.SKYTEL_ID!, src: process.env.SKYTEL_SRC! },
33
+ mobicom: {
34
+ baseUrl: process.env.MOBICOM_BASE_URL!,
35
+ serviceName: process.env.MOBICOM_SERVICE_NAME!,
36
+ username: process.env.MOBICOM_USERNAME!,
37
+ from: process.env.MOBICOM_FROM!,
38
+ },
39
+ gmobile: {
40
+ username: process.env.GMOBILE_USERNAME!,
41
+ password: process.env.GMOBILE_PASSWORD!,
42
+ from: process.env.GMOBILE_FROM!,
43
+ },
44
+ });
45
+
46
+ const result = await sms.send({ to: "99112233", message: "Таны код: 1234" });
47
+
48
+ if (!result.ok) {
49
+ console.error(result.gateway, result.error?.code, result.error?.message);
50
+ }
51
+ ```
52
+
53
+ Or let it read the environment (variable names are in [.env.example](.env.example)):
54
+
55
+ ```ts
56
+ import { configFromEnv, createSmsClient } from "mn-sms";
57
+
58
+ const sms = createSmsClient(configFromEnv());
59
+ ```
60
+
61
+ ### `send()` never throws
62
+
63
+ Every outcome comes back as a result, so success and failure can be logged the same way:
64
+
65
+ ```ts
66
+ {
67
+ ok: false,
68
+ to: "91112233", // normalized to 8 digits
69
+ operator: "skytel", // who owns the number
70
+ gateway: "skytel", // which gateway was called
71
+ statusCode: 200, // yes, 200 — see below
72
+ request: { method, url, params }, // what was sent, WITHOUT credentials
73
+ response: "ERROR: 202 not authenticated", // the gateway's own words
74
+ error: {
75
+ code: "rejected",
76
+ gatewayCode: "202",
77
+ message: "not authenticated — wrong id, or the calling IP is not on the Allowed IP List",
78
+ },
79
+ }
80
+ ```
81
+
82
+ | `error.code` | Meaning | Network call made? |
83
+ | --- | --- | --- |
84
+ | `invalid_number` | Not an 8-digit Mongolian subscriber number. | no |
85
+ | `unallocated_number` | 8 digits, but in no operator's range. | no |
86
+ | `unsupported_operator` | The number is ONDO's; no gateway for it here. | no |
87
+ | `not_configured` | The number's operator has a gateway, but you passed no config for it. | no |
88
+ | `empty_message` | — | no |
89
+ | `message_too_long` | Over the routed gateway's limit (160 for Skytel and G-Mobile). | no |
90
+ | `rejected` | The gateway answered and refused. `gatewayCode` and `response` say why. | yes |
91
+ | `timeout` | No answer within `timeoutMs`. ⚠️ **Delivery state is unknown** — the SMS may still arrive. Think before retrying an OTP. | yes |
92
+ | `network` | The request never completed (DNS, connection refused, bad base URL). | yes |
93
+
94
+ `sms.check({ to, message })` runs the "no network call" rows on their own and returns the
95
+ error or `null` — useful for validating a form before you spend a send.
96
+
97
+ ### Phone numbers
98
+
99
+ `to` is the 8-digit subscriber number. `+976`, `976`, `00976`, spaces and dashes are
100
+ stripped for you. The helpers are exported if you want them on their own:
101
+
102
+ ```ts
103
+ import { gatewayFor, normalizePhone, operatorFor } from "mn-sms";
104
+
105
+ normalizePhone("+976 9911-2233"); // "99112233"
106
+ operatorFor("55501234"); // "mobicom" (55 0–4 is Unitel, 55 5–9 is Mobicom)
107
+ gatewayFor("60112233"); // null (ONDO — no gateway)
108
+ ```
109
+
110
+ ### Ported numbers
111
+
112
+ Mongolia has number portability, and a ported number keeps its original range. Range
113
+ routing will send it to the operator it *left*, which refuses it. There is no public
114
+ ported-number database to check, so when you know the real operator, say so:
115
+
116
+ ```ts
117
+ await sms.send({ to: "99112233", message: "…", operator: "unitel" });
118
+ ```
119
+
120
+ ### Message length and cost
121
+
122
+ A single SMS holds **160** Latin characters but only **70** once it contains one Cyrillic
123
+ letter, and a longer message is split into parts that are each billed. Two helpers deal
124
+ with that. Neither is applied by `send()` — the text you pass is the text that is sent.
125
+
126
+ ```ts
127
+ import { countSegments, toLatin } from "mn-sms";
128
+
129
+ countSegments("Сайн байна уу? Таны захиалга баталгаажлаа. Хүргэлт маргааш 14 цагт очно. Баярлалаа!");
130
+ // { encoding: "UCS-2", length: 83, segments: 2, perSegment: 67 }
131
+
132
+ const text = toLatin("Сайн байна уу? Таны захиалга баталгаажлаа. Хүргэлт маргааш 14 цагт очно. Баярлалаа!");
133
+ // "Sain baina uu? Tanii zahialga batalgaajlaa. Hurgelt margaash 14 tsagt ochno. Bayarlalaa!"
134
+ countSegments(text);
135
+ // { encoding: "GSM-7", length: 88, segments: 1, perSegment: 160 }
136
+
137
+ await sms.send({ to: "99112233", message: text });
138
+ ```
139
+
140
+ `toLatin` writes Mongolian the way it is commonly typed in SMS (`ө` and `ү` → `u`,
141
+ `ы` → `ii`, `х` → `h`), and also replaces the punctuation that silently forces the
142
+ expensive encoding: curly quotes, long dashes, `…`, `№`, `₮`. Pass `{ map: { ө: "o" } }`
143
+ to change a spelling.
144
+
145
+ `countSegments` is the GSM standard's arithmetic, not an operator's invoice — ask your
146
+ operator how they count. And Skytel and G-Mobile refuse anything over 160 characters
147
+ outright, whatever the encoding.
148
+
149
+ ### Options
150
+
151
+ ```ts
152
+ createSmsClient(config, {
153
+ timeoutMs: 20_000, // default; the ceiling Skytel documents
154
+ fetch: myFetch, // inject for tests, or to go through a proxy
155
+ });
156
+ ```
157
+
158
+ ## What you need from the operators
159
+
160
+ The short version. The full account of each gateway — its wire format, every refusal code,
161
+ and what is and is not documented — is in **[docs/operators.md](docs/operators.md)**.
162
+
163
+ **1. A contract and a sender number, per operator.** Each operator sells its own "special
164
+ number" (тусгай дугаар) service to registered organisations. You sign with each one
165
+ separately and receive a sender short number plus API access. The sender is fixed to your
166
+ account: you cannot choose it per message.
167
+
168
+ **2. Your server's public IP on each operator's allowlist.** This is the requirement that
169
+ catches everyone. All four gateways check the IP the request comes from, and a request from
170
+ anywhere else is refused even with correct credentials. In practice:
171
+
172
+ - You need a **static egress IP**. Serverless platforms and autoscaled clusters that leave
173
+ through changing addresses will not work without a NAT gateway or fixed proxy.
174
+ - It will **not work from your laptop** unless that IP is registered too.
175
+ - Moving to a new server or cloud means updating four allowlists *before* you cut over.
176
+
177
+ **3. One operator's gateway only reaches that operator's subscribers.** Skytel documents this
178
+ (`ERROR: 206`); plan for it everywhere. To reach every Mongolian mobile number you need all
179
+ four accounts, which is exactly what this package's routing is for.
180
+
181
+ | | Unitel | Skytel | Mobicom | G-Mobile |
182
+ | --- | --- | --- | --- | --- |
183
+ | Credentials | API key | registration `id` | `username` + `servicename` | `username` + `password` |
184
+ | Sender | set in portal | `src` | `from` | `from` |
185
+ | IP allowlist | self-service (portal → API key → Hosts) | self-service (portal → Allowed IP List) | ask Mobicom | ask G-Mobile |
186
+ | Transport | HTTPS, POST JSON | HTTPS, GET | HTTP, GET | HTTP, GET |
187
+ | Accepted answer | HTTP 2xx | body `OK` | body `Sent` | body `0: Accepted for delivery` |
188
+ | Refusal | non-2xx | `ERROR: <code> …` under 200 | anything else | `error1`…`error6` under 200 |
189
+ | Max length | not documented | 160 chars | not documented | 160 chars |
190
+ | Cyrillic | same endpoint | separate endpoint (handled for you) | same endpoint | same endpoint |
191
+ | Numbers | 88 89 86 80, 50/55 0–4 | 90 91 96, parts of 69 | 99 95 94 85, 50/55 5–9 | 98 0–1 & 8–9, 93 97 83 0–1, 87 00–11 |
192
+
193
+ **ONDO** (60, 66, 8181) has number ranges but no gateway in this package. Sends to those
194
+ numbers return `unsupported_operator`.
195
+
196
+ ### Things worth knowing before production
197
+
198
+ - **Mobicom and G-Mobile are plain HTTP.** Your credentials and every message body cross the
199
+ network unencrypted. That is the operators' endpoint, not a choice made here.
200
+ - **`ok: true` means the gateway accepted the message, not that the handset received it.**
201
+ None of these send APIs returns a message id or a delivery report.
202
+ - **There is no rate limiting or deduplication in this package.** A public OTP endpoint
203
+ needs its own per-number cooldown, or it is an open tap on your SMS bill.
204
+ - **Long Cyrillic messages cost more.** See [Message length and cost](#message-length-and-cost).
205
+
206
+ ## Using one gateway directly
207
+
208
+ Each operator is exported on its own if you do not want routing:
209
+
210
+ ```ts
211
+ import { createSkytelGateway, resolveTransport } from "mn-sms";
212
+
213
+ const skytel = createSkytelGateway({ id, src }, resolveTransport());
214
+ await skytel.send("91112233", "hello"); // takes an already-normalized number
215
+ ```
216
+
217
+ [docs/gateways.http](docs/gateways.http) has the raw HTTP requests, for probing a gateway
218
+ with no code at all.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ npm install
224
+ npm test # node:test, with a stubbed fetch — nothing is ever sent
225
+ npm run typecheck
226
+ npm run build # dist/ — ESM, CJS and type declarations
227
+ ```
228
+
229
+ There are no live-gateway tests: a real send costs money, reaches a real subscriber and only
230
+ works from an allowlisted IP.
231
+
232
+ ### Releasing
233
+
234
+ Pushing a `v*` tag publishes to npm from GitHub Actions
235
+ ([.github/workflows/publish.yml](.github/workflows/publish.yml)). The tag must match the
236
+ version in `package.json`:
237
+
238
+ ```bash
239
+ npm version patch # bumps package.json, commits, tags
240
+ git push --follow-tags
241
+ ```
242
+
243
+ The workflow needs an `NPM_TOKEN` repository secret (an npm automation token with publish
244
+ rights). It publishes with [provenance](https://docs.npmjs.com/generating-provenance-statements),
245
+ which npm only accepts from a public repository.
246
+
247
+ ## License
248
+
249
+ MIT
package/README.mn.md ADDED
@@ -0,0 +1,255 @@
1
+ # mn-sms
2
+
3
+ [English](README.md) · **Монгол**
4
+
5
+ Монголын үүрэн холбооны операторуудын — **Юнител**, **Мобиком**, **Скайтел**,
6
+ **Жи-Мобайл** — өөрсдийнх нь SMS гарцаар нэг клиентээс мессеж илгээнэ. Та дугаар болон
7
+ текстээ өгөхөд тухайн дугаар аль операторынх болохыг тодорхойлж, тэр операторын гарц руу
8
+ хүсэлт илгээдэг.
9
+
10
+ - Гадны сан огт ашиглаагүй. Node 18+ (суурь `fetch`-ийг ашиглана). ESM болон CommonJS,
11
+ TypeScript төрөлтэй.
12
+ - Дугаарын эхний хоёр оронгоор таамаглахгүй, албан ёсны дугаарын хуваарилалтаар чиглүүлнэ.
13
+ - Гарц бүрийн жинхэнэ хариуг уншина: дөрвөн гарцын гурав нь татгалзсан үедээ ч HTTP `200`
14
+ буцаадаг.
15
+ - Нэвтрэх мэдээллийг үр дүн, алдааны мессеж, лог руу хэзээ ч гаргахгүй.
16
+
17
+ > **Энэ бол SMS зуучлагч үйлчилгээ биш.** Энэ сангийн ард дундын ямар ч бүртгэл байхгүй.
18
+ > Мессеж илгээх оператор бүртэйгээ та өөрөө гэрээ байгуулсан байх шаардлагатай —
19
+ > [Операторуудаас юу авах шаардлагатай вэ](#операторуудаас-юу-авах-шаардлагатай-вэ) хэсгийг
20
+ > үзнэ үү.
21
+
22
+ ## Суулгах
23
+
24
+ ```bash
25
+ npm install mn-sms
26
+ ```
27
+
28
+ ## Ашиглах
29
+
30
+ ```ts
31
+ import { createSmsClient } from "mn-sms";
32
+
33
+ const sms = createSmsClient({
34
+ // Зөвхөн гэрээтэй операторуудаа тохируулна.
35
+ unitel: { apiKey: process.env.UNITEL_API_KEY! },
36
+ skytel: { id: process.env.SKYTEL_ID!, src: process.env.SKYTEL_SRC! },
37
+ mobicom: {
38
+ baseUrl: process.env.MOBICOM_BASE_URL!,
39
+ serviceName: process.env.MOBICOM_SERVICE_NAME!,
40
+ username: process.env.MOBICOM_USERNAME!,
41
+ from: process.env.MOBICOM_FROM!,
42
+ },
43
+ gmobile: {
44
+ username: process.env.GMOBILE_USERNAME!,
45
+ password: process.env.GMOBILE_PASSWORD!,
46
+ from: process.env.GMOBILE_FROM!,
47
+ },
48
+ });
49
+
50
+ const result = await sms.send({ to: "99112233", message: "Таны код: 1234" });
51
+
52
+ if (!result.ok) {
53
+ console.error(result.gateway, result.error?.code, result.error?.message);
54
+ }
55
+ ```
56
+
57
+ Эсвэл орчны хувьсагчаас уншуулж болно (хувьсагчийн нэрс [.env.example](.env.example)-д бий):
58
+
59
+ ```ts
60
+ import { configFromEnv, createSmsClient } from "mn-sms";
61
+
62
+ const sms = createSmsClient(configFromEnv());
63
+ ```
64
+
65
+ ### `send()` хэзээ ч exception шидэхгүй
66
+
67
+ Амжилттай, амжилтгүй аль ч тохиолдолд үр дүнг объект хэлбэрээр буцаана. Тиймээс хоёуланг
68
+ нь нэг ижил аргаар бүртгэж болно:
69
+
70
+ ```ts
71
+ {
72
+ ok: false,
73
+ to: "91112233", // 8 оронтой болгож цэвэрлэсэн дугаар
74
+ operator: "skytel", // дугаарыг эзэмшигч оператор
75
+ gateway: "skytel", // хүсэлт очсон гарц
76
+ statusCode: 200, // тийм, 200 — доор тайлбарласан
77
+ request: { method, url, params }, // илгээсэн хүсэлт, нэвтрэх мэдээлэлГҮЙ
78
+ response: "ERROR: 202 not authenticated", // гарцын хариу, яг байгаагаар нь
79
+ error: {
80
+ code: "rejected",
81
+ gatewayCode: "202",
82
+ message: "not authenticated — wrong id, or the calling IP is not on the Allowed IP List",
83
+ },
84
+ }
85
+ ```
86
+
87
+ | `error.code` | Утга | Сүлжээгээр хүсэлт явсан уу? |
88
+ | --- | --- | --- |
89
+ | `invalid_number` | Монголын 8 оронтой дугаар биш. | үгүй |
90
+ | `unallocated_number` | 8 оронтой боловч аль ч операторын хуваарилалтад байхгүй. | үгүй |
91
+ | `unsupported_operator` | ОНДО-гийн дугаар; энэ санд гарц нь байхгүй. | үгүй |
92
+ | `not_configured` | Операторын гарц бий, гэхдээ та тохиргоог нь өгөөгүй. | үгүй |
93
+ | `empty_message` | Текст хоосон. | үгүй |
94
+ | `message_too_long` | Тухайн гарцын хязгаараас урт (Скайтел, Жи-Мобайл: 160). | үгүй |
95
+ | `rejected` | Гарц хариу өгсөн боловч татгалзсан. Шалтгааныг `gatewayCode`, `response`-оос харна. | тийм |
96
+ | `timeout` | `timeoutMs` хугацаанд хариу ирээгүй. ⚠️ **Илгээгдсэн эсэх нь тодорхойгүй** — мессеж хүрсэн байж магадгүй. OTP-г дахин илгээхээсээ өмнө бодоорой. | тийм |
97
+ | `network` | Хүсэлт дуусаагүй (DNS, холболт татгалзсан, буруу хаяг). | тийм |
98
+
99
+ `sms.check({ to, message })` нь "үгүй" гэсэн мөрүүдийн шалгалтыг дангаар нь хийж, алдаа эсвэл
100
+ `null` буцаана. Мессеж үрэхээс өмнө формоо шалгахад тохиромжтой.
101
+
102
+ ### Утасны дугаар
103
+
104
+ `to` нь 8 оронтой дугаар. `+976`, `976`, `00976`, хоосон зай, зураасыг автоматаар арилгана.
105
+ Туслах функцуудыг тусад нь ашиглаж болно:
106
+
107
+ ```ts
108
+ import { gatewayFor, normalizePhone, operatorFor } from "mn-sms";
109
+
110
+ normalizePhone("+976 9911-2233"); // "99112233"
111
+ operatorFor("55501234"); // "mobicom" (55 0–4 нь Юнител, 55 5–9 нь Мобиком)
112
+ gatewayFor("60112233"); // null (ОНДО — гарц байхгүй)
113
+ ```
114
+
115
+ ### Дугаараа хадгалан шилжсэн хэрэглэгч
116
+
117
+ Монголд дугаараа хадгалан оператор солих боломжтой бөгөөд шилжсэн дугаар анхны
118
+ хуваарилалтдаа хэвээр үлддэг. Тиймээс хуваарилалтаар чиглүүлэхэд мессеж *хуучин*
119
+ оператор руу нь очиж, татгалзсан хариу авна. Шилжсэн дугаарын нээлттэй сан байхгүй тул
120
+ жинхэнэ операторыг нь мэдэж байвал өөрөө зааж өгнө:
121
+
122
+ ```ts
123
+ await sms.send({ to: "99112233", message: "…", operator: "unitel" });
124
+ ```
125
+
126
+ ### Мессежийн урт ба өртөг
127
+
128
+ Нэг SMS-д латин үсгээр **160** тэмдэгт багтдаг бол ганц кирилл үсэг орсон даруйд ердөө
129
+ **70** болдог. Үүнээс урт мессеж хэд хэдэн хэсэгт хуваагдаж, хэсэг бүр тусдаа тооцогдоно.
130
+ Үүнд зориулсан хоёр туслах функц бий. `send()` эдгээрийг автоматаар ажиллуулахгүй — таны
131
+ өгсөн текст яг тэр чигээрээ илгээгдэнэ.
132
+
133
+ ```ts
134
+ import { countSegments, toLatin } from "mn-sms";
135
+
136
+ countSegments("Сайн байна уу? Таны захиалга баталгаажлаа. Хүргэлт маргааш 14 цагт очно. Баярлалаа!");
137
+ // { encoding: "UCS-2", length: 83, segments: 2, perSegment: 67 }
138
+
139
+ const text = toLatin("Сайн байна уу? Таны захиалга баталгаажлаа. Хүргэлт маргааш 14 цагт очно. Баярлалаа!");
140
+ // "Sain baina uu? Tanii zahialga batalgaajlaa. Hurgelt margaash 14 tsagt ochno. Bayarlalaa!"
141
+ countSegments(text);
142
+ // { encoding: "GSM-7", length: 88, segments: 1, perSegment: 160 }
143
+
144
+ await sms.send({ to: "99112233", message: text });
145
+ ```
146
+
147
+ `toLatin` нь монгол үгийг SMS-д түгээмэл бичдэг галигаар хөрвүүлнэ (`ө`, `ү` → `u`,
148
+ `ы` → `ii`, `х` → `h`). Мөн үнэтэй кодчилол руу чимээгүй шилжүүлдэг тэмдэгтүүдийг сольдог:
149
+ муруй хашилт, урт зураас, `…`, `№`, `₮`. Галигийг өөрчлөх бол `{ map: { ө: "o" } }` гэж
150
+ дамжуулна.
151
+
152
+ `countSegments` нь GSM стандартын тооцоо болохоос операторын нэхэмжлэх биш — хэрхэн
153
+ тоолдгийг оператороосоо лавлаарай. Скайтел, Жи-Мобайл хоёр кодчиллоос үл хамааран 160
154
+ тэмдэгтээс урт мессежийг шууд буцаадаг.
155
+
156
+ ### Тохиргоо
157
+
158
+ ```ts
159
+ createSmsClient(config, {
160
+ timeoutMs: 20_000, // анхны утга; Скайтелийн зааварт буй дээд хязгаар
161
+ fetch: myFetch, // тест хийх эсвэл proxy-оор дамжуулахад
162
+ });
163
+ ```
164
+
165
+ ## Операторуудаас юу авах шаардлагатай вэ
166
+
167
+ Доорх нь товч хувилбар. Гарц бүрийн хүсэлтийн хэлбэр, татгалзлын код бүр, юу нь
168
+ баримтжсан, юу нь баримтжаагүй тухай дэлгэрэнгүйг
169
+ **[docs/operators.mn.md](docs/operators.mn.md)** файлаас үзнэ үү.
170
+
171
+ **1. Оператор бүртэй гэрээ байгуулж, тусгай дугаар авна.** Оператор бүр бүртгэлтэй
172
+ байгууллагад "тусгай дугаар"-ын үйлчилгээг тус тусдаа худалддаг. Оператор тус бүртэй
173
+ гэрээ хийж, илгээгчийн богино дугаар болон API-ийн эрхээ авна. Илгээгчийн дугаар таны
174
+ бүртгэлд бэхлэгдсэн байдаг тул мессеж бүрт өөрөөр сонгох боломжгүй.
175
+
176
+ **2. Серверийнхээ гадаад IP хаягийг оператор бүрт бүртгүүлнэ.** Хүн бүр яг энэ шаардлага
177
+ дээр гацдаг. Дөрвөн гарц бүгд хүсэлт ирсэн IP хаягийг шалгадаг бөгөөд бүртгэлгүй хаягаас
178
+ ирсэн хүсэлтийг нэвтрэх мэдээлэл зөв байсан ч буцаана.
179
+
180
+ | Оператор | IP хаягаа хэрхэн бүртгүүлэх | Бүртгэлгүй хаягаас илгээвэл |
181
+ | --- | --- | --- |
182
+ | Юнител | Өөрөө, порталаас: Settings → API key → Hosts | татгалзана |
183
+ | Скайтел | Өөрөө, порталаас: Change configurations → Allowed IP List (цэгтэй таслалаар тусгаарлана) | `ERROR: 202 not authenticated` |
184
+ | Мобиком | Мобикомд хүсэлт гаргаж бүртгүүлнэ | татгалзана |
185
+ | Жи-Мобайл | Жи-Мобайлд хүсэлт гаргаж бүртгүүлнэ | `error3` |
186
+
187
+ Бодит байдал дээр энэ нь:
188
+
189
+ - **Тогтмол гадаад IP хаяг** хэрэгтэй. Гарах хаяг нь солигддог serverless платформ,
190
+ автоматаар өргөждөг кластер зэрэг нь NAT gateway эсвэл тогтмол proxy-гүйгээр ажиллахгүй.
191
+ - IP нь бүртгэлгүй бол **таны компьютерээс ажиллахгүй**.
192
+ - Шинэ сервер эсвэл клауд руу нүүхдээ дөрвөн операторын жагсаалтыг *нүүхээсээ өмнө*
193
+ шинэчилнэ.
194
+
195
+ **3. Нэг операторын гарц зөвхөн өөрийн хэрэглэгчид рүү хүргэнэ.** Скайтел үүнийг заавартаа
196
+ бичсэн байдаг (`ERROR: 206`); бусдыг ч мөн адил гэж тооцоорой. Монголын бүх гар утасны
197
+ дугаарт хүрэхийн тулд дөрвөн операторт дөрвүүлэнд нь бүртгэлтэй байх хэрэгтэй бөгөөд энэ
198
+ сангийн чиглүүлэлт яг үүнд зориулагдсан.
199
+
200
+ | | Юнител | Скайтел | Мобиком | Жи-Мобайл |
201
+ | --- | --- | --- | --- | --- |
202
+ | Нэвтрэх мэдээлэл | API key | бүртгэлийн дугаар `id` | `username` + `servicename` | `username` + `password` |
203
+ | Илгээгч | порталд тохируулна | `src` | `from` | `from` |
204
+ | Холболт | HTTPS, POST JSON | HTTPS, GET | HTTP, GET | HTTP, GET |
205
+ | Амжилттай хариу | HTTP 2xx | `OK` | `Sent` | `0: Accepted for delivery` |
206
+ | Татгалзсан хариу | 2xx биш | 200 дотор `ERROR: <код> …` | бусад бүх хариу | 200 дотор `error1`…`error6` |
207
+ | Дээд урт | баримтжаагүй | 160 тэмдэгт | баримтжаагүй | 160 тэмдэгт |
208
+ | Кирилл | нэг endpoint | тусдаа endpoint (автоматаар сонгоно) | нэг endpoint | нэг endpoint |
209
+ | Дугаар | 88 89 86 80, 50/55 0–4 | 90 91 96, 69-ийн хэсэг | 99 95 94 85, 50/55 5–9 | 98 0–1 ба 8–9, 93 97 83 0–1, 87 00–11 |
210
+
211
+ **ОНДО** (60, 66, 8181) дугаарын хуваарилалттай боловч энэ санд гарц нь байхгүй. Эдгээр
212
+ дугаар руу илгээхэд `unsupported_operator` буцна.
213
+
214
+ ### Бодит орчинд гаргахаас өмнө мэдэх зүйлс
215
+
216
+ - **Мобиком, Жи-Мобайл хоёр энгийн HTTP ашигладаг.** Таны нэвтрэх мэдээлэл болон мессеж
217
+ бүрийн агуулга сүлжээгээр шифрлэгдээгүй дамжина. Энэ нь операторын гарцын онцлог болохоос
218
+ энэ сангийн сонголт биш.
219
+ - **`ok: true` гэдэг нь гарц мессежийг хүлээн авсан гэсэн үг, утсанд хүрсэн гэсэн үг биш.**
220
+ Эдгээр API-ийн аль нь ч мессежийн дугаар эсвэл хүргэлтийн тайлан буцаадаггүй.
221
+ - **Энэ санд илгээлтийн хязгаарлалт, давхардал шалгалт байхгүй.** Нийтэд нээлттэй OTP
222
+ endpoint нь дугаар тус бүрт хүлээх хугацаатай байх ёстой, эс бөгөөс таны SMS-ийн
223
+ төлбөрийг хэн ч өсгөж чадна.
224
+ - **Урт кирилл мессеж илүү үнэтэй.** [Мессежийн урт ба өртөг](#мессежийн-урт-ба-өртөг)
225
+ хэсгийг үзнэ үү.
226
+
227
+ ## Нэг гарцыг шууд ашиглах
228
+
229
+ Чиглүүлэлт хэрэггүй бол оператор бүрийг тусад нь ашиглаж болно:
230
+
231
+ ```ts
232
+ import { createSkytelGateway, resolveTransport } from "mn-sms";
233
+
234
+ const skytel = createSkytelGateway({ id, src }, resolveTransport());
235
+ await skytel.send("91112233", "hello"); // 8 оронтой болгосон дугаар авна
236
+ ```
237
+
238
+ [docs/gateways.http](docs/gateways.http) файлд гарцуудыг кодгүйгээр шалгах түүхий HTTP
239
+ хүсэлтүүд бий.
240
+
241
+ ## Хөгжүүлэлт
242
+
243
+ ```bash
244
+ npm install
245
+ npm test # node:test, fetch-ийг орлуулсан — юу ч илгээгдэхгүй
246
+ npm run typecheck
247
+ npm run build # dist/ — ESM, CJS, төрлийн тодорхойлолт
248
+ ```
249
+
250
+ Бодит гарц руу илгээдэг тест байхгүй: жинхэнэ илгээлт мөнгө зарцуулж, бодит хэрэглэгчид
251
+ хүрдэг бөгөөд зөвхөн бүртгэлтэй IP хаягаас л ажилладаг.
252
+
253
+ ## Лиценз
254
+
255
+ MIT