@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 +91 -10
- package/dist/index.cjs +290 -11
- package/dist/index.d.cts +154 -10
- package/dist/index.d.ts +154 -10
- package/dist/index.js +284 -11
- package/package.json +5 -2
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.3.0"}`;
|
|
18
18
|
var Textbee = class {
|
|
19
19
|
#apiKey;
|
|
20
20
|
#baseUrl;
|
|
@@ -23,7 +23,7 @@ var Textbee = class {
|
|
|
23
23
|
throw new TypeError("Textbee requires an apiKey");
|
|
24
24
|
}
|
|
25
25
|
this.#apiKey = options.apiKey;
|
|
26
|
-
this.#baseUrl = (options.baseUrl
|
|
26
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
27
27
|
}
|
|
28
28
|
/** Send an SMS to one or more recipients. */
|
|
29
29
|
async sendSms(request) {
|
|
@@ -94,9 +94,10 @@ var Textbee = class {
|
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Iterate every message matching the filters, following the pagination
|
|
97
|
-
* cursor until the end.
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
97
|
+
* cursor until the end. `limit` is the page size, not a cap on the total.
|
|
98
|
+
* Use `order: 'asc'` with a `from` bound to drain forward when polling.
|
|
99
|
+
* `from` is inclusive, so a poll that resumes from the last `createdAt` sees
|
|
100
|
+
* that message again; skip ids you have already handled.
|
|
100
101
|
*
|
|
101
102
|
* ```ts
|
|
102
103
|
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
@@ -120,7 +121,11 @@ var Textbee = class {
|
|
|
120
121
|
);
|
|
121
122
|
return payload.data;
|
|
122
123
|
}
|
|
123
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Fetch a batch and every message in it, by the `smsBatchId` a send returns.
|
|
126
|
+
* Needs the id of the device that sent the batch. When textbee picked the
|
|
127
|
+
* device, use `getMessages({ smsBatchId })` instead.
|
|
128
|
+
*/
|
|
124
129
|
async getSmsBatch(deviceId, smsBatchId) {
|
|
125
130
|
const payload = await this.#request(
|
|
126
131
|
"GET",
|
|
@@ -156,6 +161,44 @@ var Textbee = class {
|
|
|
156
161
|
return payload;
|
|
157
162
|
}
|
|
158
163
|
};
|
|
164
|
+
function normalizeBaseUrl(raw) {
|
|
165
|
+
const value = raw?.trim();
|
|
166
|
+
if (!value) {
|
|
167
|
+
return DEFAULT_BASE_URL;
|
|
168
|
+
}
|
|
169
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(withScheme);
|
|
173
|
+
} catch {
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
"baseUrl is not a valid URL. Use your textbee api, for example https://sms.example.com. The /api/v1 suffix is added for you."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
179
|
+
throw new TypeError(`baseUrl must be http or https, got "${url.protocol}"`);
|
|
180
|
+
}
|
|
181
|
+
if (url.username || url.password) {
|
|
182
|
+
throw new TypeError("baseUrl must not contain a username or password");
|
|
183
|
+
}
|
|
184
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
185
|
+
let cut = -1;
|
|
186
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
187
|
+
if (segments[i].toLowerCase() === "api" && segments[i + 1].toLowerCase() === "v1") {
|
|
188
|
+
cut = i + 2;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let path;
|
|
193
|
+
if (segments.length === 0) {
|
|
194
|
+
path = ["api", "v1"];
|
|
195
|
+
} else if (cut === -1) {
|
|
196
|
+
path = segments;
|
|
197
|
+
} else {
|
|
198
|
+
path = [...segments.slice(0, cut - 2), "api", "v1"];
|
|
199
|
+
}
|
|
200
|
+
return [url.origin, ...path].join("/");
|
|
201
|
+
}
|
|
159
202
|
function toIsoString(value) {
|
|
160
203
|
return value instanceof Date ? value.toISOString() : value;
|
|
161
204
|
}
|
|
@@ -173,16 +216,240 @@ async function readBody(response) {
|
|
|
173
216
|
function errorMessage(payload, status) {
|
|
174
217
|
if (payload && typeof payload === "object") {
|
|
175
218
|
const record = payload;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
const keys = "statusCode" in record ? ["message", "error"] : ["error", "message"];
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
const value = record[key];
|
|
222
|
+
if (typeof value === "string" && value) {
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string")) {
|
|
226
|
+
return value.join("; ");
|
|
227
|
+
}
|
|
181
228
|
}
|
|
182
229
|
}
|
|
183
230
|
return `HTTP ${status}`;
|
|
184
231
|
}
|
|
185
232
|
|
|
233
|
+
// src/sms-utils.ts
|
|
234
|
+
var GSM7_BASE = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
|
|
235
|
+
\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`;
|
|
236
|
+
var GSM7_EXTENSION = "\f^{}\\[~]|\u20AC";
|
|
237
|
+
var GSM7_BASE_SET = new Set(GSM7_BASE);
|
|
238
|
+
var GSM7_EXTENSION_SET = new Set(GSM7_EXTENSION);
|
|
239
|
+
var GSM7_SINGLE_SEGMENT_LIMIT = 160;
|
|
240
|
+
var GSM7_CONCATENATED_SEGMENT_LIMIT = 153;
|
|
241
|
+
var UCS2_SINGLE_SEGMENT_LIMIT = 70;
|
|
242
|
+
var UCS2_CONCATENATED_SEGMENT_LIMIT = 67;
|
|
243
|
+
function gsm7Cost(char) {
|
|
244
|
+
if (GSM7_BASE_SET.has(char)) {
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
if (GSM7_EXTENSION_SET.has(char)) {
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
function isGsm7Char(char) {
|
|
253
|
+
return gsm7Cost(char) !== void 0;
|
|
254
|
+
}
|
|
255
|
+
function getSmsEncoding(message) {
|
|
256
|
+
for (const char of message) {
|
|
257
|
+
if (!isGsm7Char(char)) {
|
|
258
|
+
return "ucs-2";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return "gsm-7";
|
|
262
|
+
}
|
|
263
|
+
function countSmsSegments(message) {
|
|
264
|
+
const encoding = getSmsEncoding(message);
|
|
265
|
+
const costs = characterCosts(message, encoding);
|
|
266
|
+
const length = costs.reduce((total, cost) => total + cost, 0);
|
|
267
|
+
if (length === 0) {
|
|
268
|
+
return {
|
|
269
|
+
encoding,
|
|
270
|
+
length: 0,
|
|
271
|
+
segments: 0,
|
|
272
|
+
remainingInSegment: encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const singleLimit = encoding === "gsm-7" ? GSM7_SINGLE_SEGMENT_LIMIT : UCS2_SINGLE_SEGMENT_LIMIT;
|
|
276
|
+
if (length <= singleLimit) {
|
|
277
|
+
return {
|
|
278
|
+
encoding,
|
|
279
|
+
length,
|
|
280
|
+
segments: 1,
|
|
281
|
+
remainingInSegment: singleLimit - length
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const concatenatedLimit = encoding === "gsm-7" ? GSM7_CONCATENATED_SEGMENT_LIMIT : UCS2_CONCATENATED_SEGMENT_LIMIT;
|
|
285
|
+
let segments = 1;
|
|
286
|
+
let usedInSegment = 0;
|
|
287
|
+
for (const cost of costs) {
|
|
288
|
+
if (usedInSegment + cost > concatenatedLimit) {
|
|
289
|
+
segments += 1;
|
|
290
|
+
usedInSegment = cost;
|
|
291
|
+
} else {
|
|
292
|
+
usedInSegment += cost;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
encoding,
|
|
297
|
+
length,
|
|
298
|
+
segments,
|
|
299
|
+
remainingInSegment: concatenatedLimit - usedInSegment
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function findNonGsm7Characters(message) {
|
|
303
|
+
const found = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const char of message) {
|
|
305
|
+
if (!isGsm7Char(char)) {
|
|
306
|
+
found.add(char);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return Array.from(found);
|
|
310
|
+
}
|
|
311
|
+
function characterCosts(message, encoding) {
|
|
312
|
+
const costs = [];
|
|
313
|
+
for (const char of message) {
|
|
314
|
+
costs.push(encoding === "gsm-7" ? gsm7Cost(char) ?? 1 : char.length);
|
|
315
|
+
}
|
|
316
|
+
return costs;
|
|
317
|
+
}
|
|
318
|
+
var GSM7_REPLACEMENTS = {
|
|
319
|
+
// curly single quotes, prime, acute accent, backtick
|
|
320
|
+
"\u2018": "'",
|
|
321
|
+
"\u2019": "'",
|
|
322
|
+
"\u201A": "'",
|
|
323
|
+
"\u201B": "'",
|
|
324
|
+
"\u2032": "'",
|
|
325
|
+
"\xB4": "'",
|
|
326
|
+
"`": "'",
|
|
327
|
+
// curly double quotes, double prime, guillemets
|
|
328
|
+
"\u201C": '"',
|
|
329
|
+
"\u201D": '"',
|
|
330
|
+
"\u201E": '"',
|
|
331
|
+
"\u201F": '"',
|
|
332
|
+
"\u2033": '"',
|
|
333
|
+
"\xAB": '"',
|
|
334
|
+
"\xBB": '"',
|
|
335
|
+
// ellipsis
|
|
336
|
+
"\u2026": "...",
|
|
337
|
+
// hyphen variants, en dash, em dash, horizontal bar, minus sign
|
|
338
|
+
"\u2010": "-",
|
|
339
|
+
"\u2011": "-",
|
|
340
|
+
"\u2012": "-",
|
|
341
|
+
"\u2013": "-",
|
|
342
|
+
"\u2014": "-",
|
|
343
|
+
"\u2015": "-",
|
|
344
|
+
"\u2212": "-",
|
|
345
|
+
// non-breaking and typographic spaces
|
|
346
|
+
"\xA0": " ",
|
|
347
|
+
"\u2000": " ",
|
|
348
|
+
"\u2001": " ",
|
|
349
|
+
"\u2002": " ",
|
|
350
|
+
"\u2003": " ",
|
|
351
|
+
"\u2004": " ",
|
|
352
|
+
"\u2005": " ",
|
|
353
|
+
"\u2006": " ",
|
|
354
|
+
"\u2007": " ",
|
|
355
|
+
"\u2008": " ",
|
|
356
|
+
"\u2009": " ",
|
|
357
|
+
"\u200A": " ",
|
|
358
|
+
"\u202F": " ",
|
|
359
|
+
"\u205F": " ",
|
|
360
|
+
"\u3000": " ",
|
|
361
|
+
// invisible characters worth dropping outright
|
|
362
|
+
"\u200B": "",
|
|
363
|
+
"\uFEFF": "",
|
|
364
|
+
"\xAD": "",
|
|
365
|
+
// fraction slash
|
|
366
|
+
"\u2044": "/"
|
|
367
|
+
};
|
|
368
|
+
var TRANSLITERATION_EXTRAS = {
|
|
369
|
+
"\u0152": "OE",
|
|
370
|
+
"\u0153": "oe",
|
|
371
|
+
"\u0141": "L",
|
|
372
|
+
"\u0142": "l",
|
|
373
|
+
"\u0110": "D",
|
|
374
|
+
"\u0111": "d",
|
|
375
|
+
"\u0131": "i"
|
|
376
|
+
};
|
|
377
|
+
var COMBINING_MARKS = /[\u0300-\u036F]/g;
|
|
378
|
+
function sanitizeForGsm7(message, options = {}) {
|
|
379
|
+
const { transliterateAccents = false } = options;
|
|
380
|
+
let result = "";
|
|
381
|
+
for (const char of message) {
|
|
382
|
+
const replacement = GSM7_REPLACEMENTS[char];
|
|
383
|
+
if (replacement !== void 0) {
|
|
384
|
+
result += replacement;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (isGsm7Char(char)) {
|
|
388
|
+
result += char;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
result += transliterateAccents ? transliterate(char) : char;
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function transliterate(char) {
|
|
396
|
+
const extra = TRANSLITERATION_EXTRAS[char];
|
|
397
|
+
if (extra !== void 0) {
|
|
398
|
+
return extra;
|
|
399
|
+
}
|
|
400
|
+
const stripped = char.normalize("NFD").replace(COMBINING_MARKS, "");
|
|
401
|
+
if (stripped.length === 1 && /[A-Za-z]/.test(stripped)) {
|
|
402
|
+
return stripped;
|
|
403
|
+
}
|
|
404
|
+
return char;
|
|
405
|
+
}
|
|
406
|
+
var E164_PATTERN = /^\+[1-9]\d{1,14}$/;
|
|
407
|
+
var FORMATTING_CHARACTERS = /[\s.\-()]/g;
|
|
408
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
409
|
+
var COUNTRY_CODE_PATTERN = /^[1-9]\d{0,2}$/;
|
|
410
|
+
function isValidE164(input) {
|
|
411
|
+
return E164_PATTERN.test(input);
|
|
412
|
+
}
|
|
413
|
+
function normalizePhoneNumber(input, options = {}) {
|
|
414
|
+
const trimmed = input.trim();
|
|
415
|
+
if (!trimmed) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const hasPlus = trimmed.startsWith("+");
|
|
419
|
+
const withoutPlus = hasPlus ? trimmed.slice(1) : trimmed;
|
|
420
|
+
const digits = withoutPlus.replace(FORMATTING_CHARACTERS, "");
|
|
421
|
+
if (!DIGITS_ONLY.test(digits)) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (hasPlus) {
|
|
425
|
+
return asE164OrNull(`+${digits}`);
|
|
426
|
+
}
|
|
427
|
+
if (digits.startsWith("00")) {
|
|
428
|
+
return asE164OrNull(`+${digits.slice(2)}`);
|
|
429
|
+
}
|
|
430
|
+
const countryCode = normalizeCountryCode(options.defaultCountryCode);
|
|
431
|
+
if (countryCode === void 0) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const national = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
435
|
+
return asE164OrNull(`+${countryCode}${national}`);
|
|
436
|
+
}
|
|
437
|
+
function asE164OrNull(candidate) {
|
|
438
|
+
return isValidE164(candidate) ? candidate : null;
|
|
439
|
+
}
|
|
440
|
+
function normalizeCountryCode(value) {
|
|
441
|
+
if (value === void 0) {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
const code = value.startsWith("+") ? value.slice(1) : value;
|
|
445
|
+
if (!COUNTRY_CODE_PATTERN.test(code)) {
|
|
446
|
+
throw new TypeError(
|
|
447
|
+
`defaultCountryCode must be 1 to 3 digits starting with 1-9, got ${JSON.stringify(value)}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return code;
|
|
451
|
+
}
|
|
452
|
+
|
|
186
453
|
// src/webhooks.ts
|
|
187
454
|
async function verifyWebhookSignature(options) {
|
|
188
455
|
const { payload, signature, signingSecret } = options;
|
|
@@ -218,5 +485,11 @@ function constantTimeEqual(a, b) {
|
|
|
218
485
|
export {
|
|
219
486
|
Textbee,
|
|
220
487
|
TextbeeError,
|
|
488
|
+
countSmsSegments,
|
|
489
|
+
findNonGsm7Characters,
|
|
490
|
+
getSmsEncoding,
|
|
491
|
+
isValidE164,
|
|
492
|
+
normalizePhoneNumber,
|
|
493
|
+
sanitizeForGsm7,
|
|
221
494
|
verifyWebhookSignature
|
|
222
495
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textbee/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|