mn-sms 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +250 -2
- package/README.mn.md +255 -0
- package/dist/index.cjs +566 -0
- package/dist/index.d.cts +279 -0
- package/dist/index.d.ts +279 -0
- package/dist/index.js +525 -0
- package/docs/gateways.http +72 -0
- package/docs/operators.md +354 -0
- package/docs/operators.mn.md +356 -0
- package/package.json +60 -4
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,251 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mn-sms
|
|
2
2
|
|
|
3
|
-
|
|
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: a granular npm access token with read
|
|
244
|
+
and write access to this package and 2FA bypass enabled. npm caps such tokens at 90 days, so
|
|
245
|
+
it has to be renewed. It publishes with
|
|
246
|
+
[provenance](https://docs.npmjs.com/generating-provenance-statements), and npm may hold the
|
|
247
|
+
release as staged until the package owner approves it on npmjs.com.
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
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
|