@textbee/sdk 0.1.0 → 0.3.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 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: ['+251912345678'],
31
+ recipients: ['+12025550123'],
32
32
  message: 'Hello from textbee!',
33
33
  })
34
34
  ```
@@ -41,11 +41,11 @@ await textbee.sendSms({
41
41
  | --- | --- | --- |
42
42
  | `deviceId` | `string` | Which phone sends the message. Omit it and textbee uses your default device, or the enabled device with the most recent heartbeat. |
43
43
  | `simSubscriptionId` | `number` | Which SIM sends the message on a multi-SIM phone. Omit it and the phone uses its configured preferred SIM, or the system default. |
44
- | `scheduledAt` | `string \| Date` | Send later instead of now. ISO 8601 or a `Date`, must be in the future, up to 72 hours ahead. |
44
+ | `scheduledAt` | `string \| Date` | Send later instead of now. ISO 8601 or a `Date`, must be in the future. |
45
45
 
46
46
  ```js
47
47
  await textbee.sendSms({
48
- recipients: ['+251912345678'],
48
+ recipients: ['+12025550123'],
49
49
  message: 'Your appointment is tomorrow at 9am',
50
50
  deviceId: '65f0000000000000000000aa',
51
51
  simSubscriptionId: 2,
@@ -76,9 +76,9 @@ History is account-level: one call covers every device, and `deviceIds` narrows
76
76
  ```js
77
77
  // Paginated history across the whole account, filterable and searchable
78
78
  const { data, meta } = await textbee.getMessages({
79
- direction: 'received', // 'all' | 'sent' | 'received'
79
+ direction: 'sent', // 'all' | 'sent' | 'received'
80
80
  deviceIds: [deviceId], // omit for every device
81
- status: 'delivered', // delivery state; direction=sent + status=failed lists failed sends
81
+ status: 'delivered', // delivery state; status=failed lists failed sends
82
82
  search: 'invoice',
83
83
  from: '2026-08-01', // dates are UTC; datetimes need an explicit timezone
84
84
  to: '2026-09-01T00:00:00Z', // exclusive, so windows never double-count
@@ -87,7 +87,7 @@ const { data, meta } = await textbee.getMessages({
87
87
  })
88
88
 
89
89
  // direction on each message is lowercase and feeds straight back into filters
90
- data.filter((m) => m.direction === 'received')
90
+ data.filter((m) => m.direction === 'sent')
91
91
 
92
92
  // Which recipients of a bulk send failed: filter by the batch a send returned
93
93
  const { smsBatchId } = await textbee.sendSms({ recipients, message })
@@ -102,10 +102,16 @@ for await (const message of textbee.iterateMessages({ direction: 'received', ord
102
102
  // A single message and its current status
103
103
  const sms = await textbee.getSms(deviceId, smsId)
104
104
 
105
- // A whole batch, using the smsBatchId returned by sendSms
105
+ // A whole batch, using the smsBatchId returned by sendSms. This needs the id
106
+ // of the device that sent it; when textbee picked the device, use
107
+ // getMessages({ smsBatchId }) instead.
106
108
  const { batch, messages } = await textbee.getSmsBatch(deviceId, smsBatchId)
107
109
  ```
108
110
 
111
+ `getSms` and `getSmsBatch` return the `Sms` type. It differs from the `Message` that `getMessages` returns: `device` is the device id string, not an object with the phone's name and model, and `direction` is absent (read `type`).
112
+
113
+ `batch.status` and the batch counts come from the messages in the batch, so they change as each message reports. `successCount` counts messages sent or delivered, and `failureCount` counts failed ones. `pendingCount`, `dispatchedCount`, `sentCount`, `deliveredCount` and `unknownCount` break the rest down by state.
114
+
109
115
  ## Verifying webhooks
110
116
 
111
117
  textbee signs each webhook delivery with HMAC-SHA256 and sends the hex digest in the `X-Signature` header. Pass the raw request body, not a re-serialized object, whenever your framework gives you access to it.
@@ -127,6 +133,65 @@ app.post('/webhooks/textbee', express.raw({ type: 'application/json' }), async (
127
133
  })
128
134
  ```
129
135
 
136
+ ## SMS utilities
137
+
138
+ 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.
139
+
140
+ ### Segments and encoding
141
+
142
+ 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.
143
+
144
+ ```js
145
+ import { countSmsSegments, getSmsEncoding, findNonGsm7Characters } from '@textbee/sdk'
146
+
147
+ countSmsSegments('Your code is 123456')
148
+ // { encoding: 'gsm-7', length: 19, segments: 1, remainingInSegment: 141 }
149
+
150
+ countSmsSegments('Your code is 123456 🎉')
151
+ // { encoding: 'ucs-2', length: 22, segments: 1, remainingInSegment: 48 }
152
+
153
+ getSmsEncoding('plain ascii') // 'gsm-7'
154
+ findNonGsm7Characters('Hi 🎉') // ['🎉']
155
+ ```
156
+
157
+ 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.
158
+
159
+ ### Keeping messages in GSM-7
160
+
161
+ 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.
162
+
163
+ ```js
164
+ import { sanitizeForGsm7, countSmsSegments } from '@textbee/sdk'
165
+
166
+ const pasted = '“Your order shipped…”'
167
+ countSmsSegments(pasted).encoding // 'ucs-2'
168
+
169
+ const clean = sanitizeForGsm7(pasted) // '"Your order shipped..."'
170
+ countSmsSegments(clean).encoding // 'gsm-7'
171
+
172
+ // Optionally strip accents that GSM-7 does not carry. Letters it does carry,
173
+ // like é, ü, and ñ, are always left alone.
174
+ sanitizeForGsm7('naïve', { transliterateAccents: true }) // 'naive'
175
+ ```
176
+
177
+ It is best effort: characters with no safe equivalent pass through untouched. Check the result with `getSmsEncoding` and see what is left with `findNonGsm7Characters`.
178
+
179
+ ### Phone number helpers
180
+
181
+ ```js
182
+ import { isValidE164, normalizePhoneNumber } from '@textbee/sdk'
183
+
184
+ isValidE164('+12025550123') // true
185
+ isValidE164('202-555-0123') // false
186
+
187
+ normalizePhoneNumber('+1 (202) 555-0123') // '+12025550123'
188
+ normalizePhoneNumber('0012025550123') // '+12025550123'
189
+ normalizePhoneNumber('(202) 555-0123', { defaultCountryCode: '1' }) // '+12025550123'
190
+ normalizePhoneNumber('not a number') // null
191
+ ```
192
+
193
+ 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`.
194
+
130
195
  ## Errors
131
196
 
132
197
  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 +200,7 @@ Any non-2xx response throws a `TextbeeError` carrying the status and the parsed
135
200
  import { TextbeeError } from '@textbee/sdk'
136
201
 
137
202
  try {
138
- await textbee.sendSms({ recipients: ['+251912345678'], message: 'hi' })
203
+ await textbee.sendSms({ recipients: ['+12025550123'], message: 'hi' })
139
204
  } catch (error) {
140
205
  if (error instanceof TextbeeError) {
141
206
  console.error(error.status, error.message)
@@ -150,14 +215,30 @@ try {
150
215
  ```js
151
216
  new Textbee({
152
217
  apiKey: 'your-api-key',
153
- baseUrl: 'https://api.textbee.dev/api/v1', // override for self-hosted instances
218
+ baseUrl: 'https://api.textbee.dev/api/v1', // the default
154
219
  })
155
220
  ```
156
221
 
222
+ ### Self-hosted textbee
223
+
224
+ Point `baseUrl` at your own instance. The `/api/v1` suffix is optional, so all of these reach the same API:
225
+
226
+ ```js
227
+ new Textbee({ apiKey, baseUrl: 'https://sms.example.com' })
228
+ new Textbee({ apiKey, baseUrl: 'https://sms.example.com/api/v1' })
229
+ new Textbee({ apiKey, baseUrl: 'http://localhost:3001' })
230
+ ```
231
+
232
+ If the API sits under a path prefix, give the full path, for example `https://example.com/textbee/api/v1`. Any path other than `/api/v1` is used as given, so a proxy that maps `/sms` onto the API keeps working with `https://example.com/sms`. A value with no scheme gets `https://`. An invalid URL, or one with a username or password, throws a `TypeError` instead of falling back to the hosted API, so a typo never sends your key to textbee.dev.
233
+
157
234
  ## What is not covered yet
158
235
 
159
236
  The SDK focuses on sending and reading messages. Bulk send and a few device operations are still REST only, documented at [textbee.dev/docs](https://textbee.dev/docs).
160
237
 
238
+ ## Community and support
239
+
240
+ Questions or feedback? Join the community on [Discord](https://textbee.dev/discord) or email support@textbee.dev.
241
+
161
242
  ## License
162
243
 
163
- MIT. Part of the [textbee](https://github.com/vernu/textbee) project.
244
+ MIT. Part of the [textbee](https://github.com/textbee/textbee) project.
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.1.0"}`;
51
+ var CLIENT_ID = `textbee-js/${"0.3.0"}`;
46
52
  var Textbee = class {
47
53
  #apiKey;
48
54
  #baseUrl;
@@ -51,7 +57,7 @@ var Textbee = class {
51
57
  throw new TypeError("Textbee requires an apiKey");
52
58
  }
53
59
  this.#apiKey = options.apiKey;
54
- this.#baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
60
+ this.#baseUrl = normalizeBaseUrl(options.baseUrl);
55
61
  }
56
62
  /** Send an SMS to one or more recipients. */
57
63
  async sendSms(request) {
@@ -122,9 +128,10 @@ var Textbee = class {
122
128
  }
123
129
  /**
124
130
  * Iterate every message matching the filters, following the pagination
125
- * cursor until the end. Use `order: 'asc'` with a `from` bound to drain
126
- * forward when polling; keep the last message's `createdAt` (or track
127
- * `meta.nextCursor` via `getMessages`) to resume the next poll.
131
+ * cursor until the end. `limit` is the page size, not a cap on the total.
132
+ * Use `order: 'asc'` with a `from` bound to drain forward when polling.
133
+ * `from` is inclusive, so a poll that resumes from the last `createdAt` sees
134
+ * that message again; skip ids you have already handled.
128
135
  *
129
136
  * ```ts
130
137
  * for await (const message of textbee.iterateMessages({ direction: 'received' })) {
@@ -148,7 +155,11 @@ var Textbee = class {
148
155
  );
149
156
  return payload.data;
150
157
  }
151
- /** Fetch a batch and every message in it. Pair this with `smsBatchId` from a send. */
158
+ /**
159
+ * Fetch a batch and every message in it, by the `smsBatchId` a send returns.
160
+ * Needs the id of the device that sent the batch. When textbee picked the
161
+ * device, use `getMessages({ smsBatchId })` instead.
162
+ */
152
163
  async getSmsBatch(deviceId, smsBatchId) {
153
164
  const payload = await this.#request(
154
165
  "GET",
@@ -184,6 +195,44 @@ var Textbee = class {
184
195
  return payload;
185
196
  }
186
197
  };
198
+ function normalizeBaseUrl(raw) {
199
+ const value = raw?.trim();
200
+ if (!value) {
201
+ return DEFAULT_BASE_URL;
202
+ }
203
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
204
+ let url;
205
+ try {
206
+ url = new URL(withScheme);
207
+ } catch {
208
+ throw new TypeError(
209
+ "baseUrl is not a valid URL. Use your textbee api, for example https://sms.example.com. The /api/v1 suffix is added for you."
210
+ );
211
+ }
212
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
213
+ throw new TypeError(`baseUrl must be http or https, got "${url.protocol}"`);
214
+ }
215
+ if (url.username || url.password) {
216
+ throw new TypeError("baseUrl must not contain a username or password");
217
+ }
218
+ const segments = url.pathname.split("/").filter(Boolean);
219
+ let cut = -1;
220
+ for (let i = segments.length - 2; i >= 0; i--) {
221
+ if (segments[i].toLowerCase() === "api" && segments[i + 1].toLowerCase() === "v1") {
222
+ cut = i + 2;
223
+ break;
224
+ }
225
+ }
226
+ let path;
227
+ if (segments.length === 0) {
228
+ path = ["api", "v1"];
229
+ } else if (cut === -1) {
230
+ path = segments;
231
+ } else {
232
+ path = [...segments.slice(0, cut - 2), "api", "v1"];
233
+ }
234
+ return [url.origin, ...path].join("/");
235
+ }
187
236
  function toIsoString(value) {
188
237
  return value instanceof Date ? value.toISOString() : value;
189
238
  }
@@ -201,16 +250,240 @@ async function readBody(response) {
201
250
  function errorMessage(payload, status) {
202
251
  if (payload && typeof payload === "object") {
203
252
  const record = payload;
204
- if (typeof record.error === "string" && record.error) {
205
- return record.error;
206
- }
207
- if (typeof record.message === "string" && record.message) {
208
- return record.message;
253
+ const keys = "statusCode" in record ? ["message", "error"] : ["error", "message"];
254
+ for (const key of keys) {
255
+ const value = record[key];
256
+ if (typeof value === "string" && value) {
257
+ return value;
258
+ }
259
+ if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string")) {
260
+ return value.join("; ");
261
+ }
209
262
  }
210
263
  }
211
264
  return `HTTP ${status}`;
212
265
  }
213
266
 
267
+ // src/sms-utils.ts
268
+ var GSM7_BASE = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
269
+ \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`;
270
+ var GSM7_EXTENSION = "\f^{}\\[~]|\u20AC";
271
+ var GSM7_BASE_SET = new Set(GSM7_BASE);
272
+ var GSM7_EXTENSION_SET = new Set(GSM7_EXTENSION);
273
+ var GSM7_SINGLE_SEGMENT_LIMIT = 160;
274
+ var GSM7_CONCATENATED_SEGMENT_LIMIT = 153;
275
+ var UCS2_SINGLE_SEGMENT_LIMIT = 70;
276
+ var UCS2_CONCATENATED_SEGMENT_LIMIT = 67;
277
+ function gsm7Cost(char) {
278
+ if (GSM7_BASE_SET.has(char)) {
279
+ return 1;
280
+ }
281
+ if (GSM7_EXTENSION_SET.has(char)) {
282
+ return 2;
283
+ }
284
+ return void 0;
285
+ }
286
+ function isGsm7Char(char) {
287
+ return gsm7Cost(char) !== void 0;
288
+ }
289
+ function getSmsEncoding(message) {
290
+ for (const char of message) {
291
+ if (!isGsm7Char(char)) {
292
+ return "ucs-2";
293
+ }
294
+ }
295
+ return "gsm-7";
296
+ }
297
+ function countSmsSegments(message) {
298
+ const encoding = getSmsEncoding(message);
299
+ const costs = characterCosts(message, encoding);
300
+ const length = costs.reduce((total, cost) => total + cost, 0);
301
+ if (length === 0) {
302
+ return {
303
+ encoding,
304
+ length: 0,
305
+ segments: 0,
306
+ remainingInSegment: encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT
307
+ };
308
+ }
309
+ const singleLimit = encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT;
310
+ if (length <= singleLimit) {
311
+ return {
312
+ encoding,
313
+ length,
314
+ segments: 1,
315
+ remainingInSegment: singleLimit - length
316
+ };
317
+ }
318
+ const concatenatedLimit = encoding === "gsm-7" ? GSM7_CONCATENATED_SEGMENT_LIMIT : UCS2_CONCATENATED_SEGMENT_LIMIT;
319
+ let segments = 1;
320
+ let usedInSegment = 0;
321
+ for (const cost of costs) {
322
+ if (usedInSegment + cost > concatenatedLimit) {
323
+ segments += 1;
324
+ usedInSegment = cost;
325
+ } else {
326
+ usedInSegment += cost;
327
+ }
328
+ }
329
+ return {
330
+ encoding,
331
+ length,
332
+ segments,
333
+ remainingInSegment: concatenatedLimit - usedInSegment
334
+ };
335
+ }
336
+ function findNonGsm7Characters(message) {
337
+ const found = /* @__PURE__ */ new Set();
338
+ for (const char of message) {
339
+ if (!isGsm7Char(char)) {
340
+ found.add(char);
341
+ }
342
+ }
343
+ return Array.from(found);
344
+ }
345
+ function characterCosts(message, encoding) {
346
+ const costs = [];
347
+ for (const char of message) {
348
+ costs.push(encoding === "gsm-7" ? gsm7Cost(char) ?? 1 : char.length);
349
+ }
350
+ return costs;
351
+ }
352
+ var GSM7_REPLACEMENTS = {
353
+ // curly single quotes, prime, acute accent, backtick
354
+ "\u2018": "'",
355
+ "\u2019": "'",
356
+ "\u201A": "'",
357
+ "\u201B": "'",
358
+ "\u2032": "'",
359
+ "\xB4": "'",
360
+ "`": "'",
361
+ // curly double quotes, double prime, guillemets
362
+ "\u201C": '"',
363
+ "\u201D": '"',
364
+ "\u201E": '"',
365
+ "\u201F": '"',
366
+ "\u2033": '"',
367
+ "\xAB": '"',
368
+ "\xBB": '"',
369
+ // ellipsis
370
+ "\u2026": "...",
371
+ // hyphen variants, en dash, em dash, horizontal bar, minus sign
372
+ "\u2010": "-",
373
+ "\u2011": "-",
374
+ "\u2012": "-",
375
+ "\u2013": "-",
376
+ "\u2014": "-",
377
+ "\u2015": "-",
378
+ "\u2212": "-",
379
+ // non-breaking and typographic spaces
380
+ "\xA0": " ",
381
+ "\u2000": " ",
382
+ "\u2001": " ",
383
+ "\u2002": " ",
384
+ "\u2003": " ",
385
+ "\u2004": " ",
386
+ "\u2005": " ",
387
+ "\u2006": " ",
388
+ "\u2007": " ",
389
+ "\u2008": " ",
390
+ "\u2009": " ",
391
+ "\u200A": " ",
392
+ "\u202F": " ",
393
+ "\u205F": " ",
394
+ "\u3000": " ",
395
+ // invisible characters worth dropping outright
396
+ "\u200B": "",
397
+ "\uFEFF": "",
398
+ "\xAD": "",
399
+ // fraction slash
400
+ "\u2044": "/"
401
+ };
402
+ var TRANSLITERATION_EXTRAS = {
403
+ "\u0152": "OE",
404
+ "\u0153": "oe",
405
+ "\u0141": "L",
406
+ "\u0142": "l",
407
+ "\u0110": "D",
408
+ "\u0111": "d",
409
+ "\u0131": "i"
410
+ };
411
+ var COMBINING_MARKS = /[\u0300-\u036F]/g;
412
+ function sanitizeForGsm7(message, options = {}) {
413
+ const { transliterateAccents = false } = options;
414
+ let result = "";
415
+ for (const char of message) {
416
+ const replacement = GSM7_REPLACEMENTS[char];
417
+ if (replacement !== void 0) {
418
+ result += replacement;
419
+ continue;
420
+ }
421
+ if (isGsm7Char(char)) {
422
+ result += char;
423
+ continue;
424
+ }
425
+ result += transliterateAccents ? transliterate(char) : char;
426
+ }
427
+ return result;
428
+ }
429
+ function transliterate(char) {
430
+ const extra = TRANSLITERATION_EXTRAS[char];
431
+ if (extra !== void 0) {
432
+ return extra;
433
+ }
434
+ const stripped = char.normalize("NFD").replace(COMBINING_MARKS, "");
435
+ if (stripped.length === 1 && /[A-Za-z]/.test(stripped)) {
436
+ return stripped;
437
+ }
438
+ return char;
439
+ }
440
+ var E164_PATTERN = /^\+[1-9]\d{1,14}$/;
441
+ var FORMATTING_CHARACTERS = /[\s.\-()]/g;
442
+ var DIGITS_ONLY = /^\d+$/;
443
+ var COUNTRY_CODE_PATTERN = /^[1-9]\d{0,2}$/;
444
+ function isValidE164(input) {
445
+ return E164_PATTERN.test(input);
446
+ }
447
+ function normalizePhoneNumber(input, options = {}) {
448
+ const trimmed = input.trim();
449
+ if (!trimmed) {
450
+ return null;
451
+ }
452
+ const hasPlus = trimmed.startsWith("+");
453
+ const withoutPlus = hasPlus ? trimmed.slice(1) : trimmed;
454
+ const digits = withoutPlus.replace(FORMATTING_CHARACTERS, "");
455
+ if (!DIGITS_ONLY.test(digits)) {
456
+ return null;
457
+ }
458
+ if (hasPlus) {
459
+ return asE164OrNull(`+${digits}`);
460
+ }
461
+ if (digits.startsWith("00")) {
462
+ return asE164OrNull(`+${digits.slice(2)}`);
463
+ }
464
+ const countryCode = normalizeCountryCode(options.defaultCountryCode);
465
+ if (countryCode === void 0) {
466
+ return null;
467
+ }
468
+ const national = digits.startsWith("0") ? digits.slice(1) : digits;
469
+ return asE164OrNull(`+${countryCode}${national}`);
470
+ }
471
+ function asE164OrNull(candidate) {
472
+ return isValidE164(candidate) ? candidate : null;
473
+ }
474
+ function normalizeCountryCode(value) {
475
+ if (value === void 0) {
476
+ return void 0;
477
+ }
478
+ const code = value.startsWith("+") ? value.slice(1) : value;
479
+ if (!COUNTRY_CODE_PATTERN.test(code)) {
480
+ throw new TypeError(
481
+ `defaultCountryCode must be 1 to 3 digits starting with 1-9, got ${JSON.stringify(value)}`
482
+ );
483
+ }
484
+ return code;
485
+ }
486
+
214
487
  // src/webhooks.ts
215
488
  async function verifyWebhookSignature(options) {
216
489
  const { payload, signature, signingSecret } = options;
@@ -247,5 +520,11 @@ function constantTimeEqual(a, b) {
247
520
  0 && (module.exports = {
248
521
  Textbee,
249
522
  TextbeeError,
523
+ countSmsSegments,
524
+ findNonGsm7Characters,
525
+ getSmsEncoding,
526
+ isValidE164,
527
+ normalizePhoneNumber,
528
+ sanitizeForGsm7,
250
529
  verifyWebhookSignature
251
530
  });