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/dist/index.cjs ADDED
@@ -0,0 +1,566 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
24
+ GMOBILE_ERRORS: () => GMOBILE_ERRORS,
25
+ SKYTEL_ERRORS: () => SKYTEL_ERRORS,
26
+ configFromEnv: () => configFromEnv,
27
+ countSegments: () => countSegments,
28
+ createGmobileGateway: () => createGmobileGateway,
29
+ createMobicomGateway: () => createMobicomGateway,
30
+ createSkytelGateway: () => createSkytelGateway,
31
+ createSmsClient: () => createSmsClient,
32
+ createUnitelGateway: () => createUnitelGateway,
33
+ gatewayFor: () => gatewayFor,
34
+ normalizePhone: () => normalizePhone,
35
+ operatorFor: () => operatorFor,
36
+ resolveTransport: () => resolveTransport,
37
+ toLatin: () => toLatin
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/numbering.ts
42
+ var RANGES = [
43
+ [99e6, 99999999, "mobicom"],
44
+ [95e6, 95999999, "mobicom"],
45
+ [94e6, 94999999, "mobicom"],
46
+ [85e6, 85999999, "mobicom"],
47
+ [505e5, 50999999, "mobicom"],
48
+ [555e5, 55999999, "mobicom"],
49
+ [91e6, 91999999, "skytel"],
50
+ [9e7, 90999999, "skytel"],
51
+ [96e6, 96999999, "skytel"],
52
+ [69e6, 69099999, "skytel"],
53
+ [696e5, 69699999, "skytel"],
54
+ [699e5, 69999999, "skytel"],
55
+ [88e6, 88999999, "unitel"],
56
+ [86e6, 86999999, "unitel"],
57
+ [8e7, 80999999, "unitel"],
58
+ [89e6, 89999999, "unitel"],
59
+ [5e7, 50499999, "unitel"],
60
+ [55e6, 55499999, "unitel"],
61
+ [98e6, 98199999, "gmobile"],
62
+ [988e5, 98999999, "gmobile"],
63
+ [93e6, 93199999, "gmobile"],
64
+ [97e6, 97199999, "gmobile"],
65
+ [83e6, 83199999, "gmobile"],
66
+ [87e6, 87099999, "gmobile"],
67
+ [871e5, 87119999, "gmobile"],
68
+ [6e7, 60499999, "ondo"],
69
+ [605e5, 60509999, "ondo"],
70
+ [606e5, 60699999, "ondo"],
71
+ [607e5, 60709999, "ondo"],
72
+ [66e6, 66099999, "ondo"],
73
+ [661e5, 66199999, "ondo"],
74
+ [666e5, 66699999, "ondo"],
75
+ [669e5, 66999999, "ondo"],
76
+ [8181e4, 81819999, "ondo"]
77
+ ];
78
+ var GATEWAYS = {
79
+ unitel: "unitel",
80
+ skytel: "skytel",
81
+ mobicom: "mobicom",
82
+ gmobile: "gmobile",
83
+ ondo: null
84
+ };
85
+ function normalizePhone(input) {
86
+ const digits = input.replace(/[\s\-().]/g, "").replace(/^(\+|00)976/, "");
87
+ const local = digits.length === 11 && digits.startsWith("976") ? digits.slice(3) : digits;
88
+ return /^\d{8}$/.test(local) ? local : null;
89
+ }
90
+ function operatorFor(phone) {
91
+ const local = normalizePhone(phone);
92
+ if (!local) return null;
93
+ const msisdn = Number(local);
94
+ return RANGES.find(([first, last]) => msisdn >= first && msisdn <= last)?.[2] ?? null;
95
+ }
96
+ function gatewayFor(phone) {
97
+ const operator = operatorFor(phone);
98
+ return operator ? GATEWAYS[operator] : null;
99
+ }
100
+
101
+ // src/transport.ts
102
+ var DEFAULT_TIMEOUT_MS = 2e4;
103
+ function resolveTransport(options = {}) {
104
+ return {
105
+ fetch: options.fetch ?? globalThis.fetch,
106
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
107
+ };
108
+ }
109
+ function trimBase(baseUrl) {
110
+ return baseUrl.replace(/\/+$/, "");
111
+ }
112
+ function withQuery(base, fields) {
113
+ const url = new URL(base);
114
+ url.search = fields.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
115
+ return url.toString();
116
+ }
117
+ function failure(cause, request, secrets, timeoutMs) {
118
+ const raw = cause instanceof Error ? cause.message : String(cause);
119
+ const message = secrets.filter(Boolean).reduce((text, secret) => text.split(secret).join("[redacted]"), raw);
120
+ const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
121
+ return {
122
+ ok: false,
123
+ statusCode: null,
124
+ request,
125
+ response: null,
126
+ error: timedOut ? {
127
+ code: "timeout",
128
+ // The gateway said nothing, so the message may well be on its way.
129
+ message: `no answer within ${timeoutMs}ms \u2014 delivery state is unknown`
130
+ } : { code: "network", message }
131
+ };
132
+ }
133
+
134
+ // src/operators/gmobile.ts
135
+ var DEFAULT_BASE_URL = "http://sms-special.gmobile.mn";
136
+ var GMOBILE_ERRORS = {
137
+ error1: "a required parameter was not sent",
138
+ error2: "the special (sender) number does not exist",
139
+ error3: "the request came from an IP address that is not allowed",
140
+ error4: "this account's right to send has been closed",
141
+ error5: "the recipient's number is invalid",
142
+ error6: "the text is longer than 160 characters"
143
+ };
144
+ function createGmobileGateway(config, transport) {
145
+ const url = `${trimBase(config.baseUrl ?? DEFAULT_BASE_URL)}/cgi-bin/sendsms`;
146
+ return {
147
+ name: "gmobile",
148
+ maxLength: 160,
149
+ async send(to, message) {
150
+ const request = {
151
+ method: "GET",
152
+ url,
153
+ params: { from: config.from, to, text: message }
154
+ };
155
+ try {
156
+ const response = await transport.fetch(
157
+ withQuery(url, [
158
+ ["username", config.username],
159
+ ["password", config.password],
160
+ ["from", config.from],
161
+ ["to", to],
162
+ ["text", message]
163
+ ]),
164
+ { method: "GET", signal: AbortSignal.timeout(transport.timeoutMs) }
165
+ );
166
+ const body = (await response.text()).trim();
167
+ const ok = response.ok && /^0:\s*accepted\b/i.test(body);
168
+ const gatewayCode = /^error\d+$/i.exec(body)?.[0].toLowerCase();
169
+ return {
170
+ ok,
171
+ statusCode: response.status,
172
+ request,
173
+ response: body,
174
+ error: ok ? null : {
175
+ code: "rejected",
176
+ message: gatewayCode && GMOBILE_ERRORS[gatewayCode] || body || `G-Mobile answered HTTP ${response.status} with an empty body`,
177
+ ...gatewayCode ? { gatewayCode } : {}
178
+ }
179
+ };
180
+ } catch (cause) {
181
+ return failure(
182
+ cause,
183
+ request,
184
+ [config.username, config.password],
185
+ transport.timeoutMs
186
+ );
187
+ }
188
+ }
189
+ };
190
+ }
191
+
192
+ // src/operators/mobicom.ts
193
+ function createMobicomGateway(config, transport) {
194
+ const url = `${trimBase(config.baseUrl)}/smsmt/mt`;
195
+ return {
196
+ name: "mobicom",
197
+ async send(to, message) {
198
+ const request = {
199
+ method: "GET",
200
+ url,
201
+ params: {
202
+ servicename: config.serviceName,
203
+ from: config.from,
204
+ to,
205
+ msg: message
206
+ }
207
+ };
208
+ try {
209
+ const response = await transport.fetch(
210
+ withQuery(url, [
211
+ ["servicename", config.serviceName],
212
+ ["username", config.username],
213
+ ["from", config.from],
214
+ ["to", to],
215
+ ["msg", message]
216
+ ]),
217
+ { method: "GET", signal: AbortSignal.timeout(transport.timeoutMs) }
218
+ );
219
+ const body = (await response.text()).trim();
220
+ const ok = response.ok && /^sent\b/i.test(body);
221
+ return {
222
+ ok,
223
+ statusCode: response.status,
224
+ request,
225
+ response: body,
226
+ error: ok ? null : {
227
+ code: "rejected",
228
+ message: body || `Mobicom answered HTTP ${response.status} with an empty body`
229
+ }
230
+ };
231
+ } catch (cause) {
232
+ return failure(cause, request, [config.username], transport.timeoutMs);
233
+ }
234
+ }
235
+ };
236
+ }
237
+
238
+ // src/operators/skytel.ts
239
+ var DEFAULT_BASE_URL2 = "https://smsgw.skytel.mn";
240
+ var SKYTEL_ERRORS = {
241
+ "101": "a required parameter is missing",
242
+ "102": "invalid src (sender) number",
243
+ "103": "invalid dest (recipient) number",
244
+ "201": "invalid parameter(s)",
245
+ "202": "not authenticated \u2014 wrong id, or the calling IP is not on the Allowed IP List",
246
+ "203": "account disabled",
247
+ "206": "recipient is on another network \u2014 this gateway only delivers to Skytel numbers",
248
+ "207": "src is not the sender number registered to this account"
249
+ };
250
+ function endpointFor(text) {
251
+ const latinOnly = [...text].every((ch) => (ch.codePointAt(0) ?? 0) < 128);
252
+ return latinOnly ? "pushsms" : "unicode";
253
+ }
254
+ function createSkytelGateway(config, transport) {
255
+ const base = `${trimBase(config.baseUrl ?? DEFAULT_BASE_URL2)}/SMSGW-war`;
256
+ return {
257
+ name: "skytel",
258
+ maxLength: 160,
259
+ async send(to, message) {
260
+ const url = `${base}/${endpointFor(message)}`;
261
+ const request = {
262
+ method: "GET",
263
+ url,
264
+ params: { src: config.src, dest: to, text: message }
265
+ };
266
+ try {
267
+ const response = await transport.fetch(
268
+ withQuery(url, [
269
+ ["id", config.id],
270
+ ["src", config.src],
271
+ ["dest", to],
272
+ ["text", message]
273
+ ]),
274
+ { method: "GET", signal: AbortSignal.timeout(transport.timeoutMs) }
275
+ );
276
+ const body = (await response.text()).trim();
277
+ const ok = response.ok && /^ok\b/i.test(body);
278
+ const gatewayCode = /^ERROR:\s*(\d+)/i.exec(body)?.[1];
279
+ return {
280
+ ok,
281
+ statusCode: response.status,
282
+ request,
283
+ response: body,
284
+ error: ok ? null : {
285
+ code: "rejected",
286
+ message: gatewayCode && SKYTEL_ERRORS[gatewayCode] || body || `Skytel answered HTTP ${response.status} with an empty body`,
287
+ ...gatewayCode ? { gatewayCode } : {}
288
+ }
289
+ };
290
+ } catch (cause) {
291
+ return failure(cause, request, [config.id], transport.timeoutMs);
292
+ }
293
+ }
294
+ };
295
+ }
296
+
297
+ // src/operators/unitel.ts
298
+ var DEFAULT_BASE_URL3 = "https://pn.unitel.mn";
299
+ function createUnitelGateway(config, transport) {
300
+ const endpoint = `${trimBase(config.baseUrl ?? DEFAULT_BASE_URL3)}/api/message/send/sms`;
301
+ return {
302
+ name: "unitel",
303
+ async send(to, message) {
304
+ const request = {
305
+ method: "POST",
306
+ url: endpoint,
307
+ params: { to, message }
308
+ };
309
+ try {
310
+ const url = new URL(endpoint);
311
+ url.searchParams.set("enc", config.apiKey);
312
+ const response = await transport.fetch(url.toString(), {
313
+ method: "POST",
314
+ headers: { "content-type": "application/json" },
315
+ body: JSON.stringify({ to, message }),
316
+ signal: AbortSignal.timeout(transport.timeoutMs)
317
+ });
318
+ const text = await response.text();
319
+ let body = text;
320
+ try {
321
+ body = JSON.parse(text);
322
+ } catch {
323
+ }
324
+ const ok = response.ok;
325
+ return {
326
+ ok,
327
+ statusCode: response.status,
328
+ request,
329
+ response: body,
330
+ error: ok ? null : {
331
+ code: "rejected",
332
+ message: `Unitel answered HTTP ${response.status}`,
333
+ gatewayCode: String(response.status)
334
+ }
335
+ };
336
+ } catch (cause) {
337
+ return failure(cause, request, [config.apiKey], transport.timeoutMs);
338
+ }
339
+ }
340
+ };
341
+ }
342
+
343
+ // src/client.ts
344
+ function createSmsClient(config, options) {
345
+ const transport = resolveTransport(options);
346
+ const gateways = {};
347
+ if (config.unitel)
348
+ gateways.unitel = createUnitelGateway(config.unitel, transport);
349
+ if (config.skytel)
350
+ gateways.skytel = createSkytelGateway(config.skytel, transport);
351
+ if (config.mobicom)
352
+ gateways.mobicom = createMobicomGateway(config.mobicom, transport);
353
+ if (config.gmobile)
354
+ gateways.gmobile = createGmobileGateway(config.gmobile, transport);
355
+ function plan(input) {
356
+ const to = normalizePhone(input.to);
357
+ const operator = to ? operatorFor(to) : null;
358
+ const gatewayName = input.operator ?? (to ? gatewayFor(to) : null);
359
+ const gateway = gatewayName ? gateways[gatewayName] : void 0;
360
+ return { to, operator, gatewayName, gateway, error: refusal() };
361
+ function refusal() {
362
+ if (!to)
363
+ return {
364
+ code: "invalid_number",
365
+ message: "must be an 8-digit Mongolian subscriber number"
366
+ };
367
+ if (!gatewayName)
368
+ return operator ? {
369
+ code: "unsupported_operator",
370
+ message: `operator "${operator}" has no gateway in this package`
371
+ } : {
372
+ code: "unallocated_number",
373
+ message: `${to} is not in any Mongolian mobile number range`
374
+ };
375
+ if (!gateway)
376
+ return {
377
+ code: "not_configured",
378
+ message: `${to} routes to ${gatewayName}, which this client has no config for`
379
+ };
380
+ if (input.message.length === 0)
381
+ return { code: "empty_message", message: "message is empty" };
382
+ if (gateway.maxLength !== void 0 && input.message.length > gateway.maxLength)
383
+ return {
384
+ code: "message_too_long",
385
+ message: `${gatewayName} accepts at most ${gateway.maxLength} characters`
386
+ };
387
+ return null;
388
+ }
389
+ }
390
+ return {
391
+ gateways: Object.keys(gateways),
392
+ check(input) {
393
+ return plan(input).error;
394
+ },
395
+ async send(input) {
396
+ const { to, operator, gatewayName, gateway, error } = plan(input);
397
+ const head = {
398
+ to: to ?? input.to,
399
+ operator,
400
+ gateway: gatewayName
401
+ };
402
+ if (error || !gateway || !to)
403
+ return {
404
+ ...head,
405
+ ok: false,
406
+ statusCode: null,
407
+ request: null,
408
+ response: null,
409
+ error
410
+ };
411
+ return { ...head, ...await gateway.send(to, input.message) };
412
+ }
413
+ };
414
+ }
415
+
416
+ // src/env.ts
417
+ function configFromEnv(env = globalThis.process?.env ?? {}) {
418
+ const config = {};
419
+ const get = (name) => env[name] || void 0;
420
+ const unitelKey = get("UNITEL_API_KEY");
421
+ if (unitelKey)
422
+ config.unitel = { apiKey: unitelKey, baseUrl: get("UNITEL_BASE_URL") };
423
+ const skytelId = get("SKYTEL_ID");
424
+ const skytelSrc = get("SKYTEL_SRC");
425
+ if (skytelId && skytelSrc)
426
+ config.skytel = {
427
+ id: skytelId,
428
+ src: skytelSrc,
429
+ baseUrl: get("SKYTEL_BASE_URL")
430
+ };
431
+ const mobicomBase = get("MOBICOM_BASE_URL");
432
+ const mobicomService = get("MOBICOM_SERVICE_NAME");
433
+ const mobicomUser = get("MOBICOM_USERNAME");
434
+ const mobicomFrom = get("MOBICOM_FROM");
435
+ if (mobicomBase && mobicomService && mobicomUser && mobicomFrom)
436
+ config.mobicom = {
437
+ baseUrl: mobicomBase,
438
+ serviceName: mobicomService,
439
+ username: mobicomUser,
440
+ from: mobicomFrom
441
+ };
442
+ const gmobileUser = get("GMOBILE_USERNAME");
443
+ const gmobilePass = get("GMOBILE_PASSWORD");
444
+ const gmobileFrom = get("GMOBILE_FROM");
445
+ if (gmobileUser && gmobilePass && gmobileFrom)
446
+ config.gmobile = {
447
+ username: gmobileUser,
448
+ password: gmobilePass,
449
+ from: gmobileFrom,
450
+ baseUrl: get("GMOBILE_BASE_URL")
451
+ };
452
+ return config;
453
+ }
454
+
455
+ // src/text.ts
456
+ var GSM_BASIC = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
457
+ \xD8\xF8\r\xC5\xE5\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039E\xC6\xE6\xDF\xC9 !"#\xA4%&'()*+,-./0123456789:;<=>?\xA1ABCDEFGHIJKLMNOPQRSTUVWXYZ\xC4\xD6\xD1\xDC\xA7\xBFabcdefghijklmnopqrstuvwxyz\xE4\xF6\xF1\xFC\xE0`;
458
+ var GSM_EXTENDED = "\f^{}\\[~]|\u20AC";
459
+ function countSegments(message) {
460
+ const chars = [...message];
461
+ const gsm = chars.every(
462
+ (ch) => GSM_BASIC.includes(ch) || GSM_EXTENDED.includes(ch)
463
+ );
464
+ const units = chars.map(
465
+ (ch) => gsm ? GSM_EXTENDED.includes(ch) ? 2 : 1 : ch.length
466
+ );
467
+ const length = units.reduce((sum, n) => sum + n, 0);
468
+ const [single, multi] = gsm ? [160, 153] : [70, 67];
469
+ const encoding = gsm ? "GSM-7" : "UCS-2";
470
+ if (length <= single)
471
+ return { encoding, length, segments: length ? 1 : 0, perSegment: single };
472
+ let segments = 1;
473
+ let used = 0;
474
+ for (const n of units) {
475
+ if (used + n > multi) {
476
+ segments += 1;
477
+ used = 0;
478
+ }
479
+ used += n;
480
+ }
481
+ return { encoding, length, segments, perSegment: multi };
482
+ }
483
+ var CYRILLIC = {
484
+ \u0430: "a",
485
+ \u0431: "b",
486
+ \u0432: "v",
487
+ \u0433: "g",
488
+ \u0434: "d",
489
+ \u0435: "ye",
490
+ \u0451: "yo",
491
+ \u0436: "j",
492
+ \u0437: "z",
493
+ \u0438: "i",
494
+ \u0439: "i",
495
+ \u043A: "k",
496
+ \u043B: "l",
497
+ \u043C: "m",
498
+ \u043D: "n",
499
+ \u043E: "o",
500
+ \u04E9: "u",
501
+ \u043F: "p",
502
+ \u0440: "r",
503
+ \u0441: "s",
504
+ \u0442: "t",
505
+ \u0443: "u",
506
+ \u04AF: "u",
507
+ \u0444: "f",
508
+ \u0445: "h",
509
+ \u0446: "ts",
510
+ \u0447: "ch",
511
+ \u0448: "sh",
512
+ \u0449: "sh",
513
+ \u044A: "",
514
+ \u044B: "ii",
515
+ \u044C: "i",
516
+ \u044D: "e",
517
+ \u044E: "yu",
518
+ \u044F: "ya"
519
+ };
520
+ var PUNCTUATION = {
521
+ "\u201C": '"',
522
+ "\u201D": '"',
523
+ "\u201E": '"',
524
+ "\xAB": '"',
525
+ "\xBB": '"',
526
+ "\u2018": "'",
527
+ "\u2019": "'",
528
+ "\u2013": "-",
529
+ "\u2014": "-",
530
+ "\u2026": "...",
531
+ "\u2116": "No",
532
+ "\u20AE": "MNT",
533
+ "\xA0": " "
534
+ };
535
+ var isUpper = (ch) => ch !== void 0 && ch !== ch.toLowerCase();
536
+ var isLetter = (ch) => ch !== void 0 && ch.toLowerCase() !== ch.toUpperCase();
537
+ function toLatin(text, options = {}) {
538
+ const map = { ...CYRILLIC, ...PUNCTUATION, ...options.map };
539
+ const chars = [...text];
540
+ return chars.map((ch, i) => {
541
+ const latin = map[ch.toLowerCase()];
542
+ if (latin === void 0) return ch;
543
+ if (!isUpper(ch) || latin === "") return latin;
544
+ const next = chars[i + 1];
545
+ const shouting = isUpper(next) || !isLetter(next) && isUpper(chars[i - 1]);
546
+ return shouting ? latin.toUpperCase() : latin.charAt(0).toUpperCase() + latin.slice(1);
547
+ }).join("");
548
+ }
549
+ // Annotate the CommonJS export names for ESM import in node:
550
+ 0 && (module.exports = {
551
+ DEFAULT_TIMEOUT_MS,
552
+ GMOBILE_ERRORS,
553
+ SKYTEL_ERRORS,
554
+ configFromEnv,
555
+ countSegments,
556
+ createGmobileGateway,
557
+ createMobicomGateway,
558
+ createSkytelGateway,
559
+ createSmsClient,
560
+ createUnitelGateway,
561
+ gatewayFor,
562
+ normalizePhone,
563
+ operatorFor,
564
+ resolveTransport,
565
+ toLatin
566
+ });