@textbee/sdk 0.1.0 → 0.2.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/README.md +62 -3
- package/dist/index.cjs +233 -1
- package/dist/index.d.cts +92 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +227 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ import { Textbee } from '@textbee/sdk'
|
|
|
28
28
|
const textbee = new Textbee({ apiKey: process.env.TEXTBEE_API_KEY })
|
|
29
29
|
|
|
30
30
|
await textbee.sendSms({
|
|
31
|
-
recipients: ['+
|
|
31
|
+
recipients: ['+12025550123'],
|
|
32
32
|
message: 'Hello from textbee!',
|
|
33
33
|
})
|
|
34
34
|
```
|
|
@@ -45,7 +45,7 @@ await textbee.sendSms({
|
|
|
45
45
|
|
|
46
46
|
```js
|
|
47
47
|
await textbee.sendSms({
|
|
48
|
-
recipients: ['+
|
|
48
|
+
recipients: ['+12025550123'],
|
|
49
49
|
message: 'Your appointment is tomorrow at 9am',
|
|
50
50
|
deviceId: '65f0000000000000000000aa',
|
|
51
51
|
simSubscriptionId: 2,
|
|
@@ -127,6 +127,65 @@ app.post('/webhooks/textbee', express.raw({ type: 'application/json' }), async (
|
|
|
127
127
|
})
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
## SMS utilities
|
|
131
|
+
|
|
132
|
+
Pure helpers for working with SMS text and phone numbers. No API key, no network calls, and they are useful with any SMS provider, not just textbee. Import only what you need and the rest is tree-shaken away.
|
|
133
|
+
|
|
134
|
+
### Segments and encoding
|
|
135
|
+
|
|
136
|
+
Carriers bill per segment, not per message. A message stays in the 7-bit GSM alphabet at 160 characters per segment, but a single character outside that alphabet, one emoji or one curly quote, switches the whole message to UCS-2 and drops the limit to 70.
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
import { countSmsSegments, getSmsEncoding, findNonGsm7Characters } from '@textbee/sdk'
|
|
140
|
+
|
|
141
|
+
countSmsSegments('Your code is 123456')
|
|
142
|
+
// { encoding: 'gsm-7', length: 19, segments: 1, remainingInSegment: 141 }
|
|
143
|
+
|
|
144
|
+
countSmsSegments('Your code is 123456 🎉')
|
|
145
|
+
// { encoding: 'ucs-2', length: 22, segments: 1, remainingInSegment: 48 }
|
|
146
|
+
|
|
147
|
+
getSmsEncoding('plain ascii') // 'gsm-7'
|
|
148
|
+
findNonGsm7Characters('Hi 🎉') // ['🎉']
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Longer messages are split, and concatenation headers shrink each segment to 153 characters (GSM-7) or 67 (UCS-2). `remainingInSegment` counts single-unit characters, so a two-unit character such as an emoji or `€` may not fit even when it reads as 1.
|
|
152
|
+
|
|
153
|
+
### Keeping messages in GSM-7
|
|
154
|
+
|
|
155
|
+
Text pasted from a word processor or a CMS is full of curly quotes, ellipses, and non-breaking spaces. `sanitizeForGsm7` swaps them for plain equivalents so a message does not silently cost three times as much.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { sanitizeForGsm7, countSmsSegments } from '@textbee/sdk'
|
|
159
|
+
|
|
160
|
+
const pasted = '“Your order shipped…”'
|
|
161
|
+
countSmsSegments(pasted).encoding // 'ucs-2'
|
|
162
|
+
|
|
163
|
+
const clean = sanitizeForGsm7(pasted) // '"Your order shipped..."'
|
|
164
|
+
countSmsSegments(clean).encoding // 'gsm-7'
|
|
165
|
+
|
|
166
|
+
// Optionally strip accents that GSM-7 does not carry. Letters it does carry,
|
|
167
|
+
// like é, ü, and ñ, are always left alone.
|
|
168
|
+
sanitizeForGsm7('naïve', { transliterateAccents: true }) // 'naive'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
It is best effort: characters with no safe equivalent pass through untouched. Check the result with `getSmsEncoding` and see what is left with `findNonGsm7Characters`.
|
|
172
|
+
|
|
173
|
+
### Phone number helpers
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import { isValidE164, normalizePhoneNumber } from '@textbee/sdk'
|
|
177
|
+
|
|
178
|
+
isValidE164('+12025550123') // true
|
|
179
|
+
isValidE164('202-555-0123') // false
|
|
180
|
+
|
|
181
|
+
normalizePhoneNumber('+1 (202) 555-0123') // '+12025550123'
|
|
182
|
+
normalizePhoneNumber('0012025550123') // '+12025550123'
|
|
183
|
+
normalizePhoneNumber('(202) 555-0123', { defaultCountryCode: '1' }) // '+12025550123'
|
|
184
|
+
normalizePhoneNumber('not a number') // null
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
These are format-only helpers, not [libphonenumber](https://github.com/google/libphonenumber). They know nothing about country dialing plans, so a well-formed but unassigned number still passes. Input that cannot be normalized returns `null`; an unusable `defaultCountryCode` throws a `TypeError`.
|
|
188
|
+
|
|
130
189
|
## Errors
|
|
131
190
|
|
|
132
191
|
Any non-2xx response throws a `TextbeeError` carrying the status and the parsed body. Network failures reject with the underlying `fetch` error instead.
|
|
@@ -135,7 +194,7 @@ Any non-2xx response throws a `TextbeeError` carrying the status and the parsed
|
|
|
135
194
|
import { TextbeeError } from '@textbee/sdk'
|
|
136
195
|
|
|
137
196
|
try {
|
|
138
|
-
await textbee.sendSms({ recipients: ['+
|
|
197
|
+
await textbee.sendSms({ recipients: ['+12025550123'], message: 'hi' })
|
|
139
198
|
} catch (error) {
|
|
140
199
|
if (error instanceof TextbeeError) {
|
|
141
200
|
console.error(error.status, error.message)
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,12 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Textbee: () => Textbee,
|
|
24
24
|
TextbeeError: () => TextbeeError,
|
|
25
|
+
countSmsSegments: () => countSmsSegments,
|
|
26
|
+
findNonGsm7Characters: () => findNonGsm7Characters,
|
|
27
|
+
getSmsEncoding: () => getSmsEncoding,
|
|
28
|
+
isValidE164: () => isValidE164,
|
|
29
|
+
normalizePhoneNumber: () => normalizePhoneNumber,
|
|
30
|
+
sanitizeForGsm7: () => sanitizeForGsm7,
|
|
25
31
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
26
32
|
});
|
|
27
33
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -42,7 +48,7 @@ var TextbeeError = class extends Error {
|
|
|
42
48
|
|
|
43
49
|
// src/client.ts
|
|
44
50
|
var DEFAULT_BASE_URL = "https://api.textbee.dev/api/v1";
|
|
45
|
-
var CLIENT_ID = `textbee-js/${"0.
|
|
51
|
+
var CLIENT_ID = `textbee-js/${"0.2.0"}`;
|
|
46
52
|
var Textbee = class {
|
|
47
53
|
#apiKey;
|
|
48
54
|
#baseUrl;
|
|
@@ -211,6 +217,226 @@ function errorMessage(payload, status) {
|
|
|
211
217
|
return `HTTP ${status}`;
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
// src/sms-utils.ts
|
|
221
|
+
var GSM7_BASE = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
|
|
222
|
+
\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`;
|
|
223
|
+
var GSM7_EXTENSION = "\f^{}\\[~]|\u20AC";
|
|
224
|
+
var GSM7_BASE_SET = new Set(GSM7_BASE);
|
|
225
|
+
var GSM7_EXTENSION_SET = new Set(GSM7_EXTENSION);
|
|
226
|
+
var GSM7_SINGLE_SEGMENT_LIMIT = 160;
|
|
227
|
+
var GSM7_CONCATENATED_SEGMENT_LIMIT = 153;
|
|
228
|
+
var UCS2_SINGLE_SEGMENT_LIMIT = 70;
|
|
229
|
+
var UCS2_CONCATENATED_SEGMENT_LIMIT = 67;
|
|
230
|
+
function gsm7Cost(char) {
|
|
231
|
+
if (GSM7_BASE_SET.has(char)) {
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (GSM7_EXTENSION_SET.has(char)) {
|
|
235
|
+
return 2;
|
|
236
|
+
}
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
function isGsm7Char(char) {
|
|
240
|
+
return gsm7Cost(char) !== void 0;
|
|
241
|
+
}
|
|
242
|
+
function getSmsEncoding(message) {
|
|
243
|
+
for (const char of message) {
|
|
244
|
+
if (!isGsm7Char(char)) {
|
|
245
|
+
return "ucs-2";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return "gsm-7";
|
|
249
|
+
}
|
|
250
|
+
function countSmsSegments(message) {
|
|
251
|
+
const encoding = getSmsEncoding(message);
|
|
252
|
+
const costs = characterCosts(message, encoding);
|
|
253
|
+
const length = costs.reduce((total, cost) => total + cost, 0);
|
|
254
|
+
if (length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
encoding,
|
|
257
|
+
length: 0,
|
|
258
|
+
segments: 0,
|
|
259
|
+
remainingInSegment: encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const singleLimit = encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT;
|
|
263
|
+
if (length <= singleLimit) {
|
|
264
|
+
return {
|
|
265
|
+
encoding,
|
|
266
|
+
length,
|
|
267
|
+
segments: 1,
|
|
268
|
+
remainingInSegment: singleLimit - length
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const concatenatedLimit = encoding === "gsm-7" ? GSM7_CONCATENATED_SEGMENT_LIMIT : UCS2_CONCATENATED_SEGMENT_LIMIT;
|
|
272
|
+
let segments = 1;
|
|
273
|
+
let usedInSegment = 0;
|
|
274
|
+
for (const cost of costs) {
|
|
275
|
+
if (usedInSegment + cost > concatenatedLimit) {
|
|
276
|
+
segments += 1;
|
|
277
|
+
usedInSegment = cost;
|
|
278
|
+
} else {
|
|
279
|
+
usedInSegment += cost;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
encoding,
|
|
284
|
+
length,
|
|
285
|
+
segments,
|
|
286
|
+
remainingInSegment: concatenatedLimit - usedInSegment
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function findNonGsm7Characters(message) {
|
|
290
|
+
const found = /* @__PURE__ */ new Set();
|
|
291
|
+
for (const char of message) {
|
|
292
|
+
if (!isGsm7Char(char)) {
|
|
293
|
+
found.add(char);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return Array.from(found);
|
|
297
|
+
}
|
|
298
|
+
function characterCosts(message, encoding) {
|
|
299
|
+
const costs = [];
|
|
300
|
+
for (const char of message) {
|
|
301
|
+
costs.push(encoding === "gsm-7" ? gsm7Cost(char) ?? 1 : char.length);
|
|
302
|
+
}
|
|
303
|
+
return costs;
|
|
304
|
+
}
|
|
305
|
+
var GSM7_REPLACEMENTS = {
|
|
306
|
+
// curly single quotes, prime, acute accent, backtick
|
|
307
|
+
"\u2018": "'",
|
|
308
|
+
"\u2019": "'",
|
|
309
|
+
"\u201A": "'",
|
|
310
|
+
"\u201B": "'",
|
|
311
|
+
"\u2032": "'",
|
|
312
|
+
"\xB4": "'",
|
|
313
|
+
"`": "'",
|
|
314
|
+
// curly double quotes, double prime, guillemets
|
|
315
|
+
"\u201C": '"',
|
|
316
|
+
"\u201D": '"',
|
|
317
|
+
"\u201E": '"',
|
|
318
|
+
"\u201F": '"',
|
|
319
|
+
"\u2033": '"',
|
|
320
|
+
"\xAB": '"',
|
|
321
|
+
"\xBB": '"',
|
|
322
|
+
// ellipsis
|
|
323
|
+
"\u2026": "...",
|
|
324
|
+
// hyphen variants, en dash, em dash, horizontal bar, minus sign
|
|
325
|
+
"\u2010": "-",
|
|
326
|
+
"\u2011": "-",
|
|
327
|
+
"\u2012": "-",
|
|
328
|
+
"\u2013": "-",
|
|
329
|
+
"\u2014": "-",
|
|
330
|
+
"\u2015": "-",
|
|
331
|
+
"\u2212": "-",
|
|
332
|
+
// non-breaking and typographic spaces
|
|
333
|
+
"\xA0": " ",
|
|
334
|
+
"\u2000": " ",
|
|
335
|
+
"\u2001": " ",
|
|
336
|
+
"\u2002": " ",
|
|
337
|
+
"\u2003": " ",
|
|
338
|
+
"\u2004": " ",
|
|
339
|
+
"\u2005": " ",
|
|
340
|
+
"\u2006": " ",
|
|
341
|
+
"\u2007": " ",
|
|
342
|
+
"\u2008": " ",
|
|
343
|
+
"\u2009": " ",
|
|
344
|
+
"\u200A": " ",
|
|
345
|
+
"\u202F": " ",
|
|
346
|
+
"\u205F": " ",
|
|
347
|
+
"\u3000": " ",
|
|
348
|
+
// invisible characters worth dropping outright
|
|
349
|
+
"\u200B": "",
|
|
350
|
+
"\uFEFF": "",
|
|
351
|
+
"\xAD": "",
|
|
352
|
+
// fraction slash
|
|
353
|
+
"\u2044": "/"
|
|
354
|
+
};
|
|
355
|
+
var TRANSLITERATION_EXTRAS = {
|
|
356
|
+
"\u0152": "OE",
|
|
357
|
+
"\u0153": "oe",
|
|
358
|
+
"\u0141": "L",
|
|
359
|
+
"\u0142": "l",
|
|
360
|
+
"\u0110": "D",
|
|
361
|
+
"\u0111": "d",
|
|
362
|
+
"\u0131": "i"
|
|
363
|
+
};
|
|
364
|
+
var COMBINING_MARKS = /[\u0300-\u036F]/g;
|
|
365
|
+
function sanitizeForGsm7(message, options = {}) {
|
|
366
|
+
const { transliterateAccents = false } = options;
|
|
367
|
+
let result = "";
|
|
368
|
+
for (const char of message) {
|
|
369
|
+
const replacement = GSM7_REPLACEMENTS[char];
|
|
370
|
+
if (replacement !== void 0) {
|
|
371
|
+
result += replacement;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (isGsm7Char(char)) {
|
|
375
|
+
result += char;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
result += transliterateAccents ? transliterate(char) : char;
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
function transliterate(char) {
|
|
383
|
+
const extra = TRANSLITERATION_EXTRAS[char];
|
|
384
|
+
if (extra !== void 0) {
|
|
385
|
+
return extra;
|
|
386
|
+
}
|
|
387
|
+
const stripped = char.normalize("NFD").replace(COMBINING_MARKS, "");
|
|
388
|
+
if (stripped.length === 1 && /[A-Za-z]/.test(stripped)) {
|
|
389
|
+
return stripped;
|
|
390
|
+
}
|
|
391
|
+
return char;
|
|
392
|
+
}
|
|
393
|
+
var E164_PATTERN = /^\+[1-9]\d{1,14}$/;
|
|
394
|
+
var FORMATTING_CHARACTERS = /[\s.\-()]/g;
|
|
395
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
396
|
+
var COUNTRY_CODE_PATTERN = /^[1-9]\d{0,2}$/;
|
|
397
|
+
function isValidE164(input) {
|
|
398
|
+
return E164_PATTERN.test(input);
|
|
399
|
+
}
|
|
400
|
+
function normalizePhoneNumber(input, options = {}) {
|
|
401
|
+
const trimmed = input.trim();
|
|
402
|
+
if (!trimmed) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const hasPlus = trimmed.startsWith("+");
|
|
406
|
+
const withoutPlus = hasPlus ? trimmed.slice(1) : trimmed;
|
|
407
|
+
const digits = withoutPlus.replace(FORMATTING_CHARACTERS, "");
|
|
408
|
+
if (!DIGITS_ONLY.test(digits)) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (hasPlus) {
|
|
412
|
+
return asE164OrNull(`+${digits}`);
|
|
413
|
+
}
|
|
414
|
+
if (digits.startsWith("00")) {
|
|
415
|
+
return asE164OrNull(`+${digits.slice(2)}`);
|
|
416
|
+
}
|
|
417
|
+
const countryCode = normalizeCountryCode(options.defaultCountryCode);
|
|
418
|
+
if (countryCode === void 0) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const national = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
422
|
+
return asE164OrNull(`+${countryCode}${national}`);
|
|
423
|
+
}
|
|
424
|
+
function asE164OrNull(candidate) {
|
|
425
|
+
return isValidE164(candidate) ? candidate : null;
|
|
426
|
+
}
|
|
427
|
+
function normalizeCountryCode(value) {
|
|
428
|
+
if (value === void 0) {
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
431
|
+
const code = value.startsWith("+") ? value.slice(1) : value;
|
|
432
|
+
if (!COUNTRY_CODE_PATTERN.test(code)) {
|
|
433
|
+
throw new TypeError(
|
|
434
|
+
`defaultCountryCode must be 1 to 3 digits starting with 1-9, got ${JSON.stringify(value)}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return code;
|
|
438
|
+
}
|
|
439
|
+
|
|
214
440
|
// src/webhooks.ts
|
|
215
441
|
async function verifyWebhookSignature(options) {
|
|
216
442
|
const { payload, signature, signingSecret } = options;
|
|
@@ -247,5 +473,11 @@ function constantTimeEqual(a, b) {
|
|
|
247
473
|
0 && (module.exports = {
|
|
248
474
|
Textbee,
|
|
249
475
|
TextbeeError,
|
|
476
|
+
countSmsSegments,
|
|
477
|
+
findNonGsm7Characters,
|
|
478
|
+
getSmsEncoding,
|
|
479
|
+
isValidE164,
|
|
480
|
+
normalizePhoneNumber,
|
|
481
|
+
sanitizeForGsm7,
|
|
250
482
|
verifyWebhookSignature
|
|
251
483
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -234,6 +234,97 @@ declare class TextbeeError extends Error {
|
|
|
234
234
|
constructor(status: number, message: string, body?: unknown);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Pure SMS helpers. No API key, no network, no dependencies. They work with
|
|
239
|
+
* any SMS provider, not just textbee.
|
|
240
|
+
*/
|
|
241
|
+
/** How the message body will be encoded on the wire. */
|
|
242
|
+
type SmsEncoding = 'gsm-7' | 'ucs-2';
|
|
243
|
+
interface SmsSegmentInfo {
|
|
244
|
+
/** `gsm-7` when every character fits the GSM 03.38 alphabet, else `ucs-2`. */
|
|
245
|
+
encoding: SmsEncoding;
|
|
246
|
+
/**
|
|
247
|
+
* Encoded length: septets for gsm-7 (extension characters cost 2), UTF-16
|
|
248
|
+
* code units for ucs-2 (emoji and other astral characters cost 2).
|
|
249
|
+
*/
|
|
250
|
+
length: number;
|
|
251
|
+
/** Number of SMS segments the message occupies. 0 for an empty string. */
|
|
252
|
+
segments: number;
|
|
253
|
+
/**
|
|
254
|
+
* How many more single-unit characters fit before another segment is needed.
|
|
255
|
+
* A two-unit character (extension character or emoji) may not fit even when
|
|
256
|
+
* this is 1.
|
|
257
|
+
*/
|
|
258
|
+
remainingInSegment: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Detect the encoding a message requires: `gsm-7` when every character is in
|
|
262
|
+
* the GSM 03.38 alphabet or its extension table, `ucs-2` otherwise. An empty
|
|
263
|
+
* string reports `gsm-7`.
|
|
264
|
+
*/
|
|
265
|
+
declare function getSmsEncoding(message: string): SmsEncoding;
|
|
266
|
+
/**
|
|
267
|
+
* Compute encoding, encoded length, segment count, and remaining room the way
|
|
268
|
+
* a standards-following handset splits a message.
|
|
269
|
+
*
|
|
270
|
+
* Single segment limits are 160 septets (gsm-7) and 70 code units (ucs-2).
|
|
271
|
+
* Concatenated messages carry a header, shrinking each segment to 153 and 67.
|
|
272
|
+
* A two-unit character never straddles a boundary: it moves whole to the next
|
|
273
|
+
* segment and wastes one unit.
|
|
274
|
+
*/
|
|
275
|
+
declare function countSmsSegments(message: string): SmsSegmentInfo;
|
|
276
|
+
/**
|
|
277
|
+
* The unique characters forcing a message into ucs-2, in first appearance
|
|
278
|
+
* order. Empty when the message is already gsm-7 safe. Pair it with
|
|
279
|
+
* {@link sanitizeForGsm7} to see what is left after sanitizing.
|
|
280
|
+
*/
|
|
281
|
+
declare function findNonGsm7Characters(message: string): string[];
|
|
282
|
+
interface SanitizeForGsm7Options {
|
|
283
|
+
/**
|
|
284
|
+
* Strip accents from Latin letters that GSM-7 does not carry, so `ï` becomes
|
|
285
|
+
* `i`. Letters GSM-7 already has (é, ü, ñ, à and friends) are never touched.
|
|
286
|
+
* Defaults to false.
|
|
287
|
+
*/
|
|
288
|
+
transliterateAccents?: boolean;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Replace common Unicode lookalikes (curly quotes, ellipsis, exotic spaces and
|
|
292
|
+
* dashes) with GSM-7 equivalents, so pasted text does not silently fall into
|
|
293
|
+
* ucs-2 and more than double its segment count.
|
|
294
|
+
*
|
|
295
|
+
* Best effort by design: characters with no safe replacement are left alone.
|
|
296
|
+
* Check the result with {@link getSmsEncoding} and inspect leftovers with
|
|
297
|
+
* {@link findNonGsm7Characters}.
|
|
298
|
+
*/
|
|
299
|
+
declare function sanitizeForGsm7(message: string, options?: SanitizeForGsm7Options): string;
|
|
300
|
+
/**
|
|
301
|
+
* Check that a string is strictly E.164: a plus sign, a leading digit 1-9, then
|
|
302
|
+
* 1 to 14 more digits, nothing else.
|
|
303
|
+
*
|
|
304
|
+
* This is a format check only. It does not know country dialing plans, so a
|
|
305
|
+
* well-formed but unassigned number still passes.
|
|
306
|
+
*/
|
|
307
|
+
declare function isValidE164(input: string): boolean;
|
|
308
|
+
interface NormalizePhoneNumberOptions {
|
|
309
|
+
/**
|
|
310
|
+
* Country calling code applied to national-format input, with or without a
|
|
311
|
+
* leading plus: `1` and `+1` both work. With it, `(202) 555-0123` becomes
|
|
312
|
+
* `+12025550123` and one leading trunk zero is dropped. Without it,
|
|
313
|
+
* national-format input returns null.
|
|
314
|
+
*/
|
|
315
|
+
defaultCountryCode?: string;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Normalize free-form phone input to E.164, or return null when it cannot be
|
|
319
|
+
* done safely. Strips spaces, dots, hyphens and parentheses, converts a leading
|
|
320
|
+
* `00` to a plus, and optionally applies a default country code.
|
|
321
|
+
*
|
|
322
|
+
* This is a formatter, not libphonenumber: it never validates a number against
|
|
323
|
+
* a country dialing plan. Bad input returns null, a bad
|
|
324
|
+
* {@link NormalizePhoneNumberOptions.defaultCountryCode} throws.
|
|
325
|
+
*/
|
|
326
|
+
declare function normalizePhoneNumber(input: string, options?: NormalizePhoneNumberOptions): string | null;
|
|
327
|
+
|
|
237
328
|
interface VerifyWebhookSignatureOptions {
|
|
238
329
|
/**
|
|
239
330
|
* The webhook request body. Prefer the raw body string.
|
|
@@ -256,4 +347,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
256
347
|
*/
|
|
257
348
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
258
349
|
|
|
259
|
-
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, verifyWebhookSignature };
|
|
350
|
+
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -234,6 +234,97 @@ declare class TextbeeError extends Error {
|
|
|
234
234
|
constructor(status: number, message: string, body?: unknown);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Pure SMS helpers. No API key, no network, no dependencies. They work with
|
|
239
|
+
* any SMS provider, not just textbee.
|
|
240
|
+
*/
|
|
241
|
+
/** How the message body will be encoded on the wire. */
|
|
242
|
+
type SmsEncoding = 'gsm-7' | 'ucs-2';
|
|
243
|
+
interface SmsSegmentInfo {
|
|
244
|
+
/** `gsm-7` when every character fits the GSM 03.38 alphabet, else `ucs-2`. */
|
|
245
|
+
encoding: SmsEncoding;
|
|
246
|
+
/**
|
|
247
|
+
* Encoded length: septets for gsm-7 (extension characters cost 2), UTF-16
|
|
248
|
+
* code units for ucs-2 (emoji and other astral characters cost 2).
|
|
249
|
+
*/
|
|
250
|
+
length: number;
|
|
251
|
+
/** Number of SMS segments the message occupies. 0 for an empty string. */
|
|
252
|
+
segments: number;
|
|
253
|
+
/**
|
|
254
|
+
* How many more single-unit characters fit before another segment is needed.
|
|
255
|
+
* A two-unit character (extension character or emoji) may not fit even when
|
|
256
|
+
* this is 1.
|
|
257
|
+
*/
|
|
258
|
+
remainingInSegment: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Detect the encoding a message requires: `gsm-7` when every character is in
|
|
262
|
+
* the GSM 03.38 alphabet or its extension table, `ucs-2` otherwise. An empty
|
|
263
|
+
* string reports `gsm-7`.
|
|
264
|
+
*/
|
|
265
|
+
declare function getSmsEncoding(message: string): SmsEncoding;
|
|
266
|
+
/**
|
|
267
|
+
* Compute encoding, encoded length, segment count, and remaining room the way
|
|
268
|
+
* a standards-following handset splits a message.
|
|
269
|
+
*
|
|
270
|
+
* Single segment limits are 160 septets (gsm-7) and 70 code units (ucs-2).
|
|
271
|
+
* Concatenated messages carry a header, shrinking each segment to 153 and 67.
|
|
272
|
+
* A two-unit character never straddles a boundary: it moves whole to the next
|
|
273
|
+
* segment and wastes one unit.
|
|
274
|
+
*/
|
|
275
|
+
declare function countSmsSegments(message: string): SmsSegmentInfo;
|
|
276
|
+
/**
|
|
277
|
+
* The unique characters forcing a message into ucs-2, in first appearance
|
|
278
|
+
* order. Empty when the message is already gsm-7 safe. Pair it with
|
|
279
|
+
* {@link sanitizeForGsm7} to see what is left after sanitizing.
|
|
280
|
+
*/
|
|
281
|
+
declare function findNonGsm7Characters(message: string): string[];
|
|
282
|
+
interface SanitizeForGsm7Options {
|
|
283
|
+
/**
|
|
284
|
+
* Strip accents from Latin letters that GSM-7 does not carry, so `ï` becomes
|
|
285
|
+
* `i`. Letters GSM-7 already has (é, ü, ñ, à and friends) are never touched.
|
|
286
|
+
* Defaults to false.
|
|
287
|
+
*/
|
|
288
|
+
transliterateAccents?: boolean;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Replace common Unicode lookalikes (curly quotes, ellipsis, exotic spaces and
|
|
292
|
+
* dashes) with GSM-7 equivalents, so pasted text does not silently fall into
|
|
293
|
+
* ucs-2 and more than double its segment count.
|
|
294
|
+
*
|
|
295
|
+
* Best effort by design: characters with no safe replacement are left alone.
|
|
296
|
+
* Check the result with {@link getSmsEncoding} and inspect leftovers with
|
|
297
|
+
* {@link findNonGsm7Characters}.
|
|
298
|
+
*/
|
|
299
|
+
declare function sanitizeForGsm7(message: string, options?: SanitizeForGsm7Options): string;
|
|
300
|
+
/**
|
|
301
|
+
* Check that a string is strictly E.164: a plus sign, a leading digit 1-9, then
|
|
302
|
+
* 1 to 14 more digits, nothing else.
|
|
303
|
+
*
|
|
304
|
+
* This is a format check only. It does not know country dialing plans, so a
|
|
305
|
+
* well-formed but unassigned number still passes.
|
|
306
|
+
*/
|
|
307
|
+
declare function isValidE164(input: string): boolean;
|
|
308
|
+
interface NormalizePhoneNumberOptions {
|
|
309
|
+
/**
|
|
310
|
+
* Country calling code applied to national-format input, with or without a
|
|
311
|
+
* leading plus: `1` and `+1` both work. With it, `(202) 555-0123` becomes
|
|
312
|
+
* `+12025550123` and one leading trunk zero is dropped. Without it,
|
|
313
|
+
* national-format input returns null.
|
|
314
|
+
*/
|
|
315
|
+
defaultCountryCode?: string;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Normalize free-form phone input to E.164, or return null when it cannot be
|
|
319
|
+
* done safely. Strips spaces, dots, hyphens and parentheses, converts a leading
|
|
320
|
+
* `00` to a plus, and optionally applies a default country code.
|
|
321
|
+
*
|
|
322
|
+
* This is a formatter, not libphonenumber: it never validates a number against
|
|
323
|
+
* a country dialing plan. Bad input returns null, a bad
|
|
324
|
+
* {@link NormalizePhoneNumberOptions.defaultCountryCode} throws.
|
|
325
|
+
*/
|
|
326
|
+
declare function normalizePhoneNumber(input: string, options?: NormalizePhoneNumberOptions): string | null;
|
|
327
|
+
|
|
237
328
|
interface VerifyWebhookSignatureOptions {
|
|
238
329
|
/**
|
|
239
330
|
* The webhook request body. Prefer the raw body string.
|
|
@@ -256,4 +347,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
256
347
|
*/
|
|
257
348
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
258
349
|
|
|
259
|
-
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, verifyWebhookSignature };
|
|
350
|
+
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var TextbeeError = class extends Error {
|
|
|
14
14
|
|
|
15
15
|
// src/client.ts
|
|
16
16
|
var DEFAULT_BASE_URL = "https://api.textbee.dev/api/v1";
|
|
17
|
-
var CLIENT_ID = `textbee-js/${"0.
|
|
17
|
+
var CLIENT_ID = `textbee-js/${"0.2.0"}`;
|
|
18
18
|
var Textbee = class {
|
|
19
19
|
#apiKey;
|
|
20
20
|
#baseUrl;
|
|
@@ -183,6 +183,226 @@ function errorMessage(payload, status) {
|
|
|
183
183
|
return `HTTP ${status}`;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// src/sms-utils.ts
|
|
187
|
+
var GSM7_BASE = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
|
|
188
|
+
\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`;
|
|
189
|
+
var GSM7_EXTENSION = "\f^{}\\[~]|\u20AC";
|
|
190
|
+
var GSM7_BASE_SET = new Set(GSM7_BASE);
|
|
191
|
+
var GSM7_EXTENSION_SET = new Set(GSM7_EXTENSION);
|
|
192
|
+
var GSM7_SINGLE_SEGMENT_LIMIT = 160;
|
|
193
|
+
var GSM7_CONCATENATED_SEGMENT_LIMIT = 153;
|
|
194
|
+
var UCS2_SINGLE_SEGMENT_LIMIT = 70;
|
|
195
|
+
var UCS2_CONCATENATED_SEGMENT_LIMIT = 67;
|
|
196
|
+
function gsm7Cost(char) {
|
|
197
|
+
if (GSM7_BASE_SET.has(char)) {
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
if (GSM7_EXTENSION_SET.has(char)) {
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
function isGsm7Char(char) {
|
|
206
|
+
return gsm7Cost(char) !== void 0;
|
|
207
|
+
}
|
|
208
|
+
function getSmsEncoding(message) {
|
|
209
|
+
for (const char of message) {
|
|
210
|
+
if (!isGsm7Char(char)) {
|
|
211
|
+
return "ucs-2";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return "gsm-7";
|
|
215
|
+
}
|
|
216
|
+
function countSmsSegments(message) {
|
|
217
|
+
const encoding = getSmsEncoding(message);
|
|
218
|
+
const costs = characterCosts(message, encoding);
|
|
219
|
+
const length = costs.reduce((total, cost) => total + cost, 0);
|
|
220
|
+
if (length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
encoding,
|
|
223
|
+
length: 0,
|
|
224
|
+
segments: 0,
|
|
225
|
+
remainingInSegment: encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const singleLimit = encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT;
|
|
229
|
+
if (length <= singleLimit) {
|
|
230
|
+
return {
|
|
231
|
+
encoding,
|
|
232
|
+
length,
|
|
233
|
+
segments: 1,
|
|
234
|
+
remainingInSegment: singleLimit - length
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const concatenatedLimit = encoding === "gsm-7" ? GSM7_CONCATENATED_SEGMENT_LIMIT : UCS2_CONCATENATED_SEGMENT_LIMIT;
|
|
238
|
+
let segments = 1;
|
|
239
|
+
let usedInSegment = 0;
|
|
240
|
+
for (const cost of costs) {
|
|
241
|
+
if (usedInSegment + cost > concatenatedLimit) {
|
|
242
|
+
segments += 1;
|
|
243
|
+
usedInSegment = cost;
|
|
244
|
+
} else {
|
|
245
|
+
usedInSegment += cost;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
encoding,
|
|
250
|
+
length,
|
|
251
|
+
segments,
|
|
252
|
+
remainingInSegment: concatenatedLimit - usedInSegment
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function findNonGsm7Characters(message) {
|
|
256
|
+
const found = /* @__PURE__ */ new Set();
|
|
257
|
+
for (const char of message) {
|
|
258
|
+
if (!isGsm7Char(char)) {
|
|
259
|
+
found.add(char);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return Array.from(found);
|
|
263
|
+
}
|
|
264
|
+
function characterCosts(message, encoding) {
|
|
265
|
+
const costs = [];
|
|
266
|
+
for (const char of message) {
|
|
267
|
+
costs.push(encoding === "gsm-7" ? gsm7Cost(char) ?? 1 : char.length);
|
|
268
|
+
}
|
|
269
|
+
return costs;
|
|
270
|
+
}
|
|
271
|
+
var GSM7_REPLACEMENTS = {
|
|
272
|
+
// curly single quotes, prime, acute accent, backtick
|
|
273
|
+
"\u2018": "'",
|
|
274
|
+
"\u2019": "'",
|
|
275
|
+
"\u201A": "'",
|
|
276
|
+
"\u201B": "'",
|
|
277
|
+
"\u2032": "'",
|
|
278
|
+
"\xB4": "'",
|
|
279
|
+
"`": "'",
|
|
280
|
+
// curly double quotes, double prime, guillemets
|
|
281
|
+
"\u201C": '"',
|
|
282
|
+
"\u201D": '"',
|
|
283
|
+
"\u201E": '"',
|
|
284
|
+
"\u201F": '"',
|
|
285
|
+
"\u2033": '"',
|
|
286
|
+
"\xAB": '"',
|
|
287
|
+
"\xBB": '"',
|
|
288
|
+
// ellipsis
|
|
289
|
+
"\u2026": "...",
|
|
290
|
+
// hyphen variants, en dash, em dash, horizontal bar, minus sign
|
|
291
|
+
"\u2010": "-",
|
|
292
|
+
"\u2011": "-",
|
|
293
|
+
"\u2012": "-",
|
|
294
|
+
"\u2013": "-",
|
|
295
|
+
"\u2014": "-",
|
|
296
|
+
"\u2015": "-",
|
|
297
|
+
"\u2212": "-",
|
|
298
|
+
// non-breaking and typographic spaces
|
|
299
|
+
"\xA0": " ",
|
|
300
|
+
"\u2000": " ",
|
|
301
|
+
"\u2001": " ",
|
|
302
|
+
"\u2002": " ",
|
|
303
|
+
"\u2003": " ",
|
|
304
|
+
"\u2004": " ",
|
|
305
|
+
"\u2005": " ",
|
|
306
|
+
"\u2006": " ",
|
|
307
|
+
"\u2007": " ",
|
|
308
|
+
"\u2008": " ",
|
|
309
|
+
"\u2009": " ",
|
|
310
|
+
"\u200A": " ",
|
|
311
|
+
"\u202F": " ",
|
|
312
|
+
"\u205F": " ",
|
|
313
|
+
"\u3000": " ",
|
|
314
|
+
// invisible characters worth dropping outright
|
|
315
|
+
"\u200B": "",
|
|
316
|
+
"\uFEFF": "",
|
|
317
|
+
"\xAD": "",
|
|
318
|
+
// fraction slash
|
|
319
|
+
"\u2044": "/"
|
|
320
|
+
};
|
|
321
|
+
var TRANSLITERATION_EXTRAS = {
|
|
322
|
+
"\u0152": "OE",
|
|
323
|
+
"\u0153": "oe",
|
|
324
|
+
"\u0141": "L",
|
|
325
|
+
"\u0142": "l",
|
|
326
|
+
"\u0110": "D",
|
|
327
|
+
"\u0111": "d",
|
|
328
|
+
"\u0131": "i"
|
|
329
|
+
};
|
|
330
|
+
var COMBINING_MARKS = /[\u0300-\u036F]/g;
|
|
331
|
+
function sanitizeForGsm7(message, options = {}) {
|
|
332
|
+
const { transliterateAccents = false } = options;
|
|
333
|
+
let result = "";
|
|
334
|
+
for (const char of message) {
|
|
335
|
+
const replacement = GSM7_REPLACEMENTS[char];
|
|
336
|
+
if (replacement !== void 0) {
|
|
337
|
+
result += replacement;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (isGsm7Char(char)) {
|
|
341
|
+
result += char;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
result += transliterateAccents ? transliterate(char) : char;
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
function transliterate(char) {
|
|
349
|
+
const extra = TRANSLITERATION_EXTRAS[char];
|
|
350
|
+
if (extra !== void 0) {
|
|
351
|
+
return extra;
|
|
352
|
+
}
|
|
353
|
+
const stripped = char.normalize("NFD").replace(COMBINING_MARKS, "");
|
|
354
|
+
if (stripped.length === 1 && /[A-Za-z]/.test(stripped)) {
|
|
355
|
+
return stripped;
|
|
356
|
+
}
|
|
357
|
+
return char;
|
|
358
|
+
}
|
|
359
|
+
var E164_PATTERN = /^\+[1-9]\d{1,14}$/;
|
|
360
|
+
var FORMATTING_CHARACTERS = /[\s.\-()]/g;
|
|
361
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
362
|
+
var COUNTRY_CODE_PATTERN = /^[1-9]\d{0,2}$/;
|
|
363
|
+
function isValidE164(input) {
|
|
364
|
+
return E164_PATTERN.test(input);
|
|
365
|
+
}
|
|
366
|
+
function normalizePhoneNumber(input, options = {}) {
|
|
367
|
+
const trimmed = input.trim();
|
|
368
|
+
if (!trimmed) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const hasPlus = trimmed.startsWith("+");
|
|
372
|
+
const withoutPlus = hasPlus ? trimmed.slice(1) : trimmed;
|
|
373
|
+
const digits = withoutPlus.replace(FORMATTING_CHARACTERS, "");
|
|
374
|
+
if (!DIGITS_ONLY.test(digits)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (hasPlus) {
|
|
378
|
+
return asE164OrNull(`+${digits}`);
|
|
379
|
+
}
|
|
380
|
+
if (digits.startsWith("00")) {
|
|
381
|
+
return asE164OrNull(`+${digits.slice(2)}`);
|
|
382
|
+
}
|
|
383
|
+
const countryCode = normalizeCountryCode(options.defaultCountryCode);
|
|
384
|
+
if (countryCode === void 0) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const national = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
388
|
+
return asE164OrNull(`+${countryCode}${national}`);
|
|
389
|
+
}
|
|
390
|
+
function asE164OrNull(candidate) {
|
|
391
|
+
return isValidE164(candidate) ? candidate : null;
|
|
392
|
+
}
|
|
393
|
+
function normalizeCountryCode(value) {
|
|
394
|
+
if (value === void 0) {
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
const code = value.startsWith("+") ? value.slice(1) : value;
|
|
398
|
+
if (!COUNTRY_CODE_PATTERN.test(code)) {
|
|
399
|
+
throw new TypeError(
|
|
400
|
+
`defaultCountryCode must be 1 to 3 digits starting with 1-9, got ${JSON.stringify(value)}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return code;
|
|
404
|
+
}
|
|
405
|
+
|
|
186
406
|
// src/webhooks.ts
|
|
187
407
|
async function verifyWebhookSignature(options) {
|
|
188
408
|
const { payload, signature, signingSecret } = options;
|
|
@@ -218,5 +438,11 @@ function constantTimeEqual(a, b) {
|
|
|
218
438
|
export {
|
|
219
439
|
Textbee,
|
|
220
440
|
TextbeeError,
|
|
441
|
+
countSmsSegments,
|
|
442
|
+
findNonGsm7Characters,
|
|
443
|
+
getSmsEncoding,
|
|
444
|
+
isValidE164,
|
|
445
|
+
normalizePhoneNumber,
|
|
446
|
+
sanitizeForGsm7,
|
|
221
447
|
verifyWebhookSignature
|
|
222
448
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textbee/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Official JavaScript SDK for textbee.dev, the open source SMS gateway",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"packageManager": "pnpm@9.14.2",
|
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
"sms",
|
|
45
45
|
"sms-gateway",
|
|
46
46
|
"textbee",
|
|
47
|
-
"android"
|
|
47
|
+
"android",
|
|
48
|
+
"sms-segments",
|
|
49
|
+
"gsm-7",
|
|
50
|
+
"e164"
|
|
48
51
|
],
|
|
49
52
|
"scripts": {
|
|
50
53
|
"build": "tsup",
|