@textbee/sdk 0.0.3 → 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 +84 -9
- package/dist/index.cjs +275 -14
- package/dist/index.d.cts +172 -16
- package/dist/index.d.ts +172 -16
- package/dist/index.js +269 -14
- 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,
|
|
@@ -71,17 +71,33 @@ await textbee.setDefaultDevice(deviceId)
|
|
|
71
71
|
|
|
72
72
|
## Messages and delivery status
|
|
73
73
|
|
|
74
|
+
History is account-level: one call covers every device, and `deviceIds` narrows it.
|
|
75
|
+
|
|
74
76
|
```js
|
|
75
|
-
// Paginated history, filterable and searchable
|
|
76
|
-
const { data, meta } = await textbee.getMessages(
|
|
77
|
-
|
|
77
|
+
// Paginated history across the whole account, filterable and searchable
|
|
78
|
+
const { data, meta } = await textbee.getMessages({
|
|
79
|
+
direction: 'received', // 'all' | 'sent' | 'received'
|
|
80
|
+
deviceIds: [deviceId], // omit for every device
|
|
81
|
+
status: 'delivered', // delivery state; direction=sent + status=failed lists failed sends
|
|
82
|
+
search: 'invoice',
|
|
83
|
+
from: '2026-08-01', // dates are UTC; datetimes need an explicit timezone
|
|
84
|
+
to: '2026-09-01T00:00:00Z', // exclusive, so windows never double-count
|
|
78
85
|
page: 1,
|
|
79
86
|
limit: 50,
|
|
80
|
-
search: 'invoice',
|
|
81
87
|
})
|
|
82
88
|
|
|
83
|
-
//
|
|
84
|
-
data.filter((m) => m.
|
|
89
|
+
// direction on each message is lowercase and feeds straight back into filters
|
|
90
|
+
data.filter((m) => m.direction === 'received')
|
|
91
|
+
|
|
92
|
+
// Which recipients of a bulk send failed: filter by the batch a send returned
|
|
93
|
+
const { smsBatchId } = await textbee.sendSms({ recipients, message })
|
|
94
|
+
const failed = await textbee.getMessages({ smsBatchId, status: 'failed' })
|
|
95
|
+
|
|
96
|
+
// Drain everything matching a filter: iterateMessages follows the
|
|
97
|
+
// pagination cursor for you until there is nothing left
|
|
98
|
+
for await (const message of textbee.iterateMessages({ direction: 'received', order: 'asc' })) {
|
|
99
|
+
console.log(message.sender, message.message)
|
|
100
|
+
}
|
|
85
101
|
|
|
86
102
|
// A single message and its current status
|
|
87
103
|
const sms = await textbee.getSms(deviceId, smsId)
|
|
@@ -111,6 +127,65 @@ app.post('/webhooks/textbee', express.raw({ type: 'application/json' }), async (
|
|
|
111
127
|
})
|
|
112
128
|
```
|
|
113
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
|
+
|
|
114
189
|
## Errors
|
|
115
190
|
|
|
116
191
|
Any non-2xx response throws a `TextbeeError` carrying the status and the parsed body. Network failures reject with the underlying `fetch` error instead.
|
|
@@ -119,7 +194,7 @@ Any non-2xx response throws a `TextbeeError` carrying the status and the parsed
|
|
|
119
194
|
import { TextbeeError } from '@textbee/sdk'
|
|
120
195
|
|
|
121
196
|
try {
|
|
122
|
-
await textbee.sendSms({ recipients: ['+
|
|
197
|
+
await textbee.sendSms({ recipients: ['+12025550123'], message: 'hi' })
|
|
123
198
|
} catch (error) {
|
|
124
199
|
if (error instanceof TextbeeError) {
|
|
125
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.0
|
|
51
|
+
var CLIENT_ID = `textbee-js/${"0.2.0"}`;
|
|
46
52
|
var Textbee = class {
|
|
47
53
|
#apiKey;
|
|
48
54
|
#baseUrl;
|
|
@@ -99,20 +105,46 @@ var Textbee = class {
|
|
|
99
105
|
);
|
|
100
106
|
return payload.data;
|
|
101
107
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Page through the account's sent and received messages, filtered by
|
|
110
|
+
* device, batch, direction, status, text, and time range.
|
|
111
|
+
*/
|
|
112
|
+
async getMessages(options = {}) {
|
|
113
|
+
return await this.#request("GET", "/gateway/messages", {
|
|
114
|
+
query: {
|
|
115
|
+
deviceIds: options.deviceIds?.length ? options.deviceIds.join(",") : void 0,
|
|
116
|
+
smsBatchId: options.smsBatchId,
|
|
117
|
+
direction: options.direction,
|
|
118
|
+
status: options.status,
|
|
119
|
+
search: options.search,
|
|
120
|
+
from: toIsoString(options.from),
|
|
121
|
+
to: toIsoString(options.to),
|
|
122
|
+
order: options.order,
|
|
123
|
+
page: options.page,
|
|
124
|
+
limit: options.limit,
|
|
125
|
+
cursor: options.cursor
|
|
114
126
|
}
|
|
115
|
-
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Iterate every message matching the filters, following the pagination
|
|
131
|
+
* cursor until the end. Use `order: 'asc'` with a `from` bound to drain
|
|
132
|
+
* forward when polling; keep the last message's `createdAt` (or track
|
|
133
|
+
* `meta.nextCursor` via `getMessages`) to resume the next poll.
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
137
|
+
* handle(message)
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
async *iterateMessages(options = {}) {
|
|
142
|
+
let page = await this.getMessages(options);
|
|
143
|
+
yield* page.data;
|
|
144
|
+
while (page.meta.nextCursor) {
|
|
145
|
+
page = await this.getMessages({ ...options, cursor: page.meta.nextCursor });
|
|
146
|
+
yield* page.data;
|
|
147
|
+
}
|
|
116
148
|
}
|
|
117
149
|
/** Fetch a single message, including its delivery status. */
|
|
118
150
|
async getSms(deviceId, smsId) {
|
|
@@ -158,6 +190,9 @@ var Textbee = class {
|
|
|
158
190
|
return payload;
|
|
159
191
|
}
|
|
160
192
|
};
|
|
193
|
+
function toIsoString(value) {
|
|
194
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
195
|
+
}
|
|
161
196
|
async function readBody(response) {
|
|
162
197
|
const text = await response.text();
|
|
163
198
|
if (!text) {
|
|
@@ -182,6 +217,226 @@ function errorMessage(payload, status) {
|
|
|
182
217
|
return `HTTP ${status}`;
|
|
183
218
|
}
|
|
184
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
|
+
|
|
185
440
|
// src/webhooks.ts
|
|
186
441
|
async function verifyWebhookSignature(options) {
|
|
187
442
|
const { payload, signature, signingSecret } = options;
|
|
@@ -218,5 +473,11 @@ function constantTimeEqual(a, b) {
|
|
|
218
473
|
0 && (module.exports = {
|
|
219
474
|
Textbee,
|
|
220
475
|
TextbeeError,
|
|
476
|
+
countSmsSegments,
|
|
477
|
+
findNonGsm7Characters,
|
|
478
|
+
getSmsEncoding,
|
|
479
|
+
isValidE164,
|
|
480
|
+
normalizePhoneNumber,
|
|
481
|
+
sanitizeForGsm7,
|
|
221
482
|
verifyWebhookSignature
|
|
222
483
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -54,16 +54,42 @@ interface SendSmsImmediateResponse {
|
|
|
54
54
|
* with `'smsBatchId' in result` before reading batch fields.
|
|
55
55
|
*/
|
|
56
56
|
type SendSmsResponse = SendSmsQueuedResponse | SendSmsImmediateResponse;
|
|
57
|
-
interface
|
|
57
|
+
interface ListMessagesOptions {
|
|
58
|
+
/** Only messages from these devices. Omit for every device on the account. */
|
|
59
|
+
deviceIds?: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Only messages from this batch, using the `smsBatchId` a send returns.
|
|
62
|
+
* Combine with `status: 'failed'` to list the recipients that failed.
|
|
63
|
+
*/
|
|
64
|
+
smsBatchId?: string;
|
|
58
65
|
/** Filter by direction. Defaults to `all`. */
|
|
59
|
-
|
|
60
|
-
/**
|
|
66
|
+
direction?: 'all' | 'sent' | 'received';
|
|
67
|
+
/** Filter by delivery state, e.g. `failed` to list sends that failed. */
|
|
68
|
+
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
69
|
+
/** Free text match across the message body, recipient, and sender. */
|
|
70
|
+
search?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Inclusive lower bound on when the platform stored the message. A string
|
|
73
|
+
* must carry an explicit timezone (`2026-08-01T00:00:00Z`); a bare date like
|
|
74
|
+
* `2026-08-01` is read as UTC midnight.
|
|
75
|
+
*/
|
|
76
|
+
from?: string | Date;
|
|
77
|
+
/** Exclusive upper bound, same formats as `from`. */
|
|
78
|
+
to?: string | Date;
|
|
79
|
+
/** `desc` (default) for newest first, `asc` to walk forward when polling. */
|
|
80
|
+
order?: 'desc' | 'asc';
|
|
81
|
+
/** 1-based page number. Defaults to 1. Mutually exclusive with `cursor`. */
|
|
61
82
|
page?: number;
|
|
62
83
|
/** Items per page. Defaults to 50, capped at 100 by the API. */
|
|
63
84
|
limit?: number;
|
|
64
|
-
/**
|
|
65
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Opaque position from a previous response's `meta.nextCursor`. When set,
|
|
87
|
+
* `meta` switches to cursor mode and omits the total count.
|
|
88
|
+
*/
|
|
89
|
+
cursor?: string;
|
|
66
90
|
}
|
|
91
|
+
/** Everything `iterateMessages` accepts: a filter set, minus the paging knobs it drives itself. */
|
|
92
|
+
type IterateMessagesOptions = Omit<ListMessagesOptions, 'page' | 'cursor'>;
|
|
67
93
|
interface Device {
|
|
68
94
|
_id: string;
|
|
69
95
|
name?: string;
|
|
@@ -82,14 +108,29 @@ interface Device {
|
|
|
82
108
|
createdAt?: string;
|
|
83
109
|
updatedAt?: string;
|
|
84
110
|
}
|
|
111
|
+
interface MessageDevice {
|
|
112
|
+
_id: string;
|
|
113
|
+
brand?: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
buildId?: string;
|
|
116
|
+
enabled?: boolean;
|
|
117
|
+
}
|
|
85
118
|
interface Message {
|
|
86
119
|
_id: string;
|
|
87
120
|
message: string;
|
|
88
121
|
/**
|
|
89
|
-
* Direction, uppercase.
|
|
90
|
-
*
|
|
122
|
+
* Direction, uppercase.
|
|
123
|
+
*
|
|
124
|
+
* @deprecated Read `direction` instead: it is lowercase and matches the
|
|
125
|
+
* `direction` filter, so a response value feeds straight back into a query.
|
|
91
126
|
*/
|
|
92
127
|
type: 'SENT' | 'RECEIVED';
|
|
128
|
+
/** Direction, lowercase. Matches the `direction` filter values. */
|
|
129
|
+
direction?: 'sent' | 'received';
|
|
130
|
+
/** Message channel. Currently always `sms`; absent means `sms`. */
|
|
131
|
+
channel?: string;
|
|
132
|
+
/** The sending or receiving device, populated by the history endpoints. */
|
|
133
|
+
device?: MessageDevice;
|
|
93
134
|
/** Lowercase, and absent on messages stored before status tracking. */
|
|
94
135
|
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
95
136
|
sender?: string;
|
|
@@ -120,15 +161,23 @@ interface SmsBatch {
|
|
|
120
161
|
createdAt?: string;
|
|
121
162
|
updatedAt?: string;
|
|
122
163
|
}
|
|
123
|
-
|
|
124
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Meta for the account-level message list. Page mode carries the counters and
|
|
166
|
+
* the cursor fields; cursor mode carries `limit`, `nextCursor`, and `hasMore`
|
|
167
|
+
* only, skipping the expensive total count.
|
|
168
|
+
*/
|
|
169
|
+
interface MessageListMeta {
|
|
125
170
|
limit: number;
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
page?: number;
|
|
172
|
+
total?: number;
|
|
173
|
+
totalPages?: number;
|
|
174
|
+
/** Opaque position after the last message. Null on the final page. */
|
|
175
|
+
nextCursor?: string | null;
|
|
176
|
+
hasMore?: boolean;
|
|
128
177
|
}
|
|
129
|
-
interface
|
|
178
|
+
interface MessageList {
|
|
130
179
|
data: Message[];
|
|
131
|
-
meta:
|
|
180
|
+
meta: MessageListMeta;
|
|
132
181
|
}
|
|
133
182
|
interface SmsBatchResult {
|
|
134
183
|
batch: SmsBatch;
|
|
@@ -148,8 +197,24 @@ declare class Textbee {
|
|
|
148
197
|
getDevice(deviceId: string): Promise<Device>;
|
|
149
198
|
/** Make a device the default sender for requests that omit `deviceId`. */
|
|
150
199
|
setDefaultDevice(deviceId: string): Promise<Device>;
|
|
151
|
-
/**
|
|
152
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Page through the account's sent and received messages, filtered by
|
|
202
|
+
* device, batch, direction, status, text, and time range.
|
|
203
|
+
*/
|
|
204
|
+
getMessages(options?: ListMessagesOptions): Promise<MessageList>;
|
|
205
|
+
/**
|
|
206
|
+
* Iterate every message matching the filters, following the pagination
|
|
207
|
+
* cursor until the end. Use `order: 'asc'` with a `from` bound to drain
|
|
208
|
+
* forward when polling; keep the last message's `createdAt` (or track
|
|
209
|
+
* `meta.nextCursor` via `getMessages`) to resume the next poll.
|
|
210
|
+
*
|
|
211
|
+
* ```ts
|
|
212
|
+
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
213
|
+
* handle(message)
|
|
214
|
+
* }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
iterateMessages(options?: IterateMessagesOptions): AsyncGenerator<Message, void, undefined>;
|
|
153
218
|
/** Fetch a single message, including its delivery status. */
|
|
154
219
|
getSms(deviceId: string, smsId: string): Promise<Message>;
|
|
155
220
|
/** Fetch a batch and every message in it. Pair this with `smsBatchId` from a send. */
|
|
@@ -169,6 +234,97 @@ declare class TextbeeError extends Error {
|
|
|
169
234
|
constructor(status: number, message: string, body?: unknown);
|
|
170
235
|
}
|
|
171
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
|
+
|
|
172
328
|
interface VerifyWebhookSignatureOptions {
|
|
173
329
|
/**
|
|
174
330
|
* The webhook request body. Prefer the raw body string.
|
|
@@ -191,4 +347,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
191
347
|
*/
|
|
192
348
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
193
349
|
|
|
194
|
-
export { type Device, type
|
|
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
|
@@ -54,16 +54,42 @@ interface SendSmsImmediateResponse {
|
|
|
54
54
|
* with `'smsBatchId' in result` before reading batch fields.
|
|
55
55
|
*/
|
|
56
56
|
type SendSmsResponse = SendSmsQueuedResponse | SendSmsImmediateResponse;
|
|
57
|
-
interface
|
|
57
|
+
interface ListMessagesOptions {
|
|
58
|
+
/** Only messages from these devices. Omit for every device on the account. */
|
|
59
|
+
deviceIds?: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Only messages from this batch, using the `smsBatchId` a send returns.
|
|
62
|
+
* Combine with `status: 'failed'` to list the recipients that failed.
|
|
63
|
+
*/
|
|
64
|
+
smsBatchId?: string;
|
|
58
65
|
/** Filter by direction. Defaults to `all`. */
|
|
59
|
-
|
|
60
|
-
/**
|
|
66
|
+
direction?: 'all' | 'sent' | 'received';
|
|
67
|
+
/** Filter by delivery state, e.g. `failed` to list sends that failed. */
|
|
68
|
+
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
69
|
+
/** Free text match across the message body, recipient, and sender. */
|
|
70
|
+
search?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Inclusive lower bound on when the platform stored the message. A string
|
|
73
|
+
* must carry an explicit timezone (`2026-08-01T00:00:00Z`); a bare date like
|
|
74
|
+
* `2026-08-01` is read as UTC midnight.
|
|
75
|
+
*/
|
|
76
|
+
from?: string | Date;
|
|
77
|
+
/** Exclusive upper bound, same formats as `from`. */
|
|
78
|
+
to?: string | Date;
|
|
79
|
+
/** `desc` (default) for newest first, `asc` to walk forward when polling. */
|
|
80
|
+
order?: 'desc' | 'asc';
|
|
81
|
+
/** 1-based page number. Defaults to 1. Mutually exclusive with `cursor`. */
|
|
61
82
|
page?: number;
|
|
62
83
|
/** Items per page. Defaults to 50, capped at 100 by the API. */
|
|
63
84
|
limit?: number;
|
|
64
|
-
/**
|
|
65
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Opaque position from a previous response's `meta.nextCursor`. When set,
|
|
87
|
+
* `meta` switches to cursor mode and omits the total count.
|
|
88
|
+
*/
|
|
89
|
+
cursor?: string;
|
|
66
90
|
}
|
|
91
|
+
/** Everything `iterateMessages` accepts: a filter set, minus the paging knobs it drives itself. */
|
|
92
|
+
type IterateMessagesOptions = Omit<ListMessagesOptions, 'page' | 'cursor'>;
|
|
67
93
|
interface Device {
|
|
68
94
|
_id: string;
|
|
69
95
|
name?: string;
|
|
@@ -82,14 +108,29 @@ interface Device {
|
|
|
82
108
|
createdAt?: string;
|
|
83
109
|
updatedAt?: string;
|
|
84
110
|
}
|
|
111
|
+
interface MessageDevice {
|
|
112
|
+
_id: string;
|
|
113
|
+
brand?: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
buildId?: string;
|
|
116
|
+
enabled?: boolean;
|
|
117
|
+
}
|
|
85
118
|
interface Message {
|
|
86
119
|
_id: string;
|
|
87
120
|
message: string;
|
|
88
121
|
/**
|
|
89
|
-
* Direction, uppercase.
|
|
90
|
-
*
|
|
122
|
+
* Direction, uppercase.
|
|
123
|
+
*
|
|
124
|
+
* @deprecated Read `direction` instead: it is lowercase and matches the
|
|
125
|
+
* `direction` filter, so a response value feeds straight back into a query.
|
|
91
126
|
*/
|
|
92
127
|
type: 'SENT' | 'RECEIVED';
|
|
128
|
+
/** Direction, lowercase. Matches the `direction` filter values. */
|
|
129
|
+
direction?: 'sent' | 'received';
|
|
130
|
+
/** Message channel. Currently always `sms`; absent means `sms`. */
|
|
131
|
+
channel?: string;
|
|
132
|
+
/** The sending or receiving device, populated by the history endpoints. */
|
|
133
|
+
device?: MessageDevice;
|
|
93
134
|
/** Lowercase, and absent on messages stored before status tracking. */
|
|
94
135
|
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
95
136
|
sender?: string;
|
|
@@ -120,15 +161,23 @@ interface SmsBatch {
|
|
|
120
161
|
createdAt?: string;
|
|
121
162
|
updatedAt?: string;
|
|
122
163
|
}
|
|
123
|
-
|
|
124
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Meta for the account-level message list. Page mode carries the counters and
|
|
166
|
+
* the cursor fields; cursor mode carries `limit`, `nextCursor`, and `hasMore`
|
|
167
|
+
* only, skipping the expensive total count.
|
|
168
|
+
*/
|
|
169
|
+
interface MessageListMeta {
|
|
125
170
|
limit: number;
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
page?: number;
|
|
172
|
+
total?: number;
|
|
173
|
+
totalPages?: number;
|
|
174
|
+
/** Opaque position after the last message. Null on the final page. */
|
|
175
|
+
nextCursor?: string | null;
|
|
176
|
+
hasMore?: boolean;
|
|
128
177
|
}
|
|
129
|
-
interface
|
|
178
|
+
interface MessageList {
|
|
130
179
|
data: Message[];
|
|
131
|
-
meta:
|
|
180
|
+
meta: MessageListMeta;
|
|
132
181
|
}
|
|
133
182
|
interface SmsBatchResult {
|
|
134
183
|
batch: SmsBatch;
|
|
@@ -148,8 +197,24 @@ declare class Textbee {
|
|
|
148
197
|
getDevice(deviceId: string): Promise<Device>;
|
|
149
198
|
/** Make a device the default sender for requests that omit `deviceId`. */
|
|
150
199
|
setDefaultDevice(deviceId: string): Promise<Device>;
|
|
151
|
-
/**
|
|
152
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Page through the account's sent and received messages, filtered by
|
|
202
|
+
* device, batch, direction, status, text, and time range.
|
|
203
|
+
*/
|
|
204
|
+
getMessages(options?: ListMessagesOptions): Promise<MessageList>;
|
|
205
|
+
/**
|
|
206
|
+
* Iterate every message matching the filters, following the pagination
|
|
207
|
+
* cursor until the end. Use `order: 'asc'` with a `from` bound to drain
|
|
208
|
+
* forward when polling; keep the last message's `createdAt` (or track
|
|
209
|
+
* `meta.nextCursor` via `getMessages`) to resume the next poll.
|
|
210
|
+
*
|
|
211
|
+
* ```ts
|
|
212
|
+
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
213
|
+
* handle(message)
|
|
214
|
+
* }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
iterateMessages(options?: IterateMessagesOptions): AsyncGenerator<Message, void, undefined>;
|
|
153
218
|
/** Fetch a single message, including its delivery status. */
|
|
154
219
|
getSms(deviceId: string, smsId: string): Promise<Message>;
|
|
155
220
|
/** Fetch a batch and every message in it. Pair this with `smsBatchId` from a send. */
|
|
@@ -169,6 +234,97 @@ declare class TextbeeError extends Error {
|
|
|
169
234
|
constructor(status: number, message: string, body?: unknown);
|
|
170
235
|
}
|
|
171
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
|
+
|
|
172
328
|
interface VerifyWebhookSignatureOptions {
|
|
173
329
|
/**
|
|
174
330
|
* The webhook request body. Prefer the raw body string.
|
|
@@ -191,4 +347,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
191
347
|
*/
|
|
192
348
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
193
349
|
|
|
194
|
-
export { type Device, type
|
|
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.0
|
|
17
|
+
var CLIENT_ID = `textbee-js/${"0.2.0"}`;
|
|
18
18
|
var Textbee = class {
|
|
19
19
|
#apiKey;
|
|
20
20
|
#baseUrl;
|
|
@@ -71,20 +71,46 @@ var Textbee = class {
|
|
|
71
71
|
);
|
|
72
72
|
return payload.data;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Page through the account's sent and received messages, filtered by
|
|
76
|
+
* device, batch, direction, status, text, and time range.
|
|
77
|
+
*/
|
|
78
|
+
async getMessages(options = {}) {
|
|
79
|
+
return await this.#request("GET", "/gateway/messages", {
|
|
80
|
+
query: {
|
|
81
|
+
deviceIds: options.deviceIds?.length ? options.deviceIds.join(",") : void 0,
|
|
82
|
+
smsBatchId: options.smsBatchId,
|
|
83
|
+
direction: options.direction,
|
|
84
|
+
status: options.status,
|
|
85
|
+
search: options.search,
|
|
86
|
+
from: toIsoString(options.from),
|
|
87
|
+
to: toIsoString(options.to),
|
|
88
|
+
order: options.order,
|
|
89
|
+
page: options.page,
|
|
90
|
+
limit: options.limit,
|
|
91
|
+
cursor: options.cursor
|
|
86
92
|
}
|
|
87
|
-
);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Iterate every message matching the filters, following the pagination
|
|
97
|
+
* cursor until the end. Use `order: 'asc'` with a `from` bound to drain
|
|
98
|
+
* forward when polling; keep the last message's `createdAt` (or track
|
|
99
|
+
* `meta.nextCursor` via `getMessages`) to resume the next poll.
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
103
|
+
* handle(message)
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
async *iterateMessages(options = {}) {
|
|
108
|
+
let page = await this.getMessages(options);
|
|
109
|
+
yield* page.data;
|
|
110
|
+
while (page.meta.nextCursor) {
|
|
111
|
+
page = await this.getMessages({ ...options, cursor: page.meta.nextCursor });
|
|
112
|
+
yield* page.data;
|
|
113
|
+
}
|
|
88
114
|
}
|
|
89
115
|
/** Fetch a single message, including its delivery status. */
|
|
90
116
|
async getSms(deviceId, smsId) {
|
|
@@ -130,6 +156,9 @@ var Textbee = class {
|
|
|
130
156
|
return payload;
|
|
131
157
|
}
|
|
132
158
|
};
|
|
159
|
+
function toIsoString(value) {
|
|
160
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
161
|
+
}
|
|
133
162
|
async function readBody(response) {
|
|
134
163
|
const text = await response.text();
|
|
135
164
|
if (!text) {
|
|
@@ -154,6 +183,226 @@ function errorMessage(payload, status) {
|
|
|
154
183
|
return `HTTP ${status}`;
|
|
155
184
|
}
|
|
156
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
|
+
|
|
157
406
|
// src/webhooks.ts
|
|
158
407
|
async function verifyWebhookSignature(options) {
|
|
159
408
|
const { payload, signature, signingSecret } = options;
|
|
@@ -189,5 +438,11 @@ function constantTimeEqual(a, b) {
|
|
|
189
438
|
export {
|
|
190
439
|
Textbee,
|
|
191
440
|
TextbeeError,
|
|
441
|
+
countSmsSegments,
|
|
442
|
+
findNonGsm7Characters,
|
|
443
|
+
getSmsEncoding,
|
|
444
|
+
isValidE164,
|
|
445
|
+
normalizePhoneNumber,
|
|
446
|
+
sanitizeForGsm7,
|
|
192
447
|
verifyWebhookSignature
|
|
193
448
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textbee/sdk",
|
|
3
|
-
"version": "0.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",
|