@sendly/node 1.0.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 +329 -0
- package/dist/index.d.mts +1204 -0
- package/dist/index.d.ts +1204 -0
- package/dist/index.js +1181 -0
- package/dist/index.mjs +1121 -0
- package/package.json +54 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SendlyError = class _SendlyError extends Error {
|
|
3
|
+
/**
|
|
4
|
+
* Machine-readable error code
|
|
5
|
+
*/
|
|
6
|
+
code;
|
|
7
|
+
/**
|
|
8
|
+
* HTTP status code (if applicable)
|
|
9
|
+
*/
|
|
10
|
+
statusCode;
|
|
11
|
+
/**
|
|
12
|
+
* Raw API response (if applicable)
|
|
13
|
+
*/
|
|
14
|
+
response;
|
|
15
|
+
constructor(message, code, statusCode, response) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "SendlyError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.response = response;
|
|
21
|
+
if (Error.captureStackTrace) {
|
|
22
|
+
Error.captureStackTrace(this, this.constructor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a SendlyError from an API response
|
|
27
|
+
*/
|
|
28
|
+
static fromResponse(statusCode, response) {
|
|
29
|
+
const message = response.message || "An unknown error occurred";
|
|
30
|
+
const code = response.error || "internal_error";
|
|
31
|
+
switch (code) {
|
|
32
|
+
case "unauthorized":
|
|
33
|
+
case "invalid_auth_format":
|
|
34
|
+
case "invalid_key_format":
|
|
35
|
+
case "invalid_api_key":
|
|
36
|
+
case "key_revoked":
|
|
37
|
+
case "key_expired":
|
|
38
|
+
case "insufficient_permissions":
|
|
39
|
+
return new AuthenticationError(message, code, statusCode, response);
|
|
40
|
+
case "rate_limit_exceeded":
|
|
41
|
+
return new RateLimitError(
|
|
42
|
+
message,
|
|
43
|
+
response.retryAfter || 60,
|
|
44
|
+
statusCode,
|
|
45
|
+
response
|
|
46
|
+
);
|
|
47
|
+
case "insufficient_credits":
|
|
48
|
+
return new InsufficientCreditsError(
|
|
49
|
+
message,
|
|
50
|
+
response.creditsNeeded || 0,
|
|
51
|
+
response.currentBalance || 0,
|
|
52
|
+
statusCode,
|
|
53
|
+
response
|
|
54
|
+
);
|
|
55
|
+
case "invalid_request":
|
|
56
|
+
case "unsupported_destination":
|
|
57
|
+
return new ValidationError(message, code, statusCode, response);
|
|
58
|
+
case "not_found":
|
|
59
|
+
return new NotFoundError(message, statusCode, response);
|
|
60
|
+
default:
|
|
61
|
+
return new _SendlyError(message, code, statusCode, response);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var AuthenticationError = class extends SendlyError {
|
|
66
|
+
constructor(message, code = "unauthorized", statusCode, response) {
|
|
67
|
+
super(message, code, statusCode, response);
|
|
68
|
+
this.name = "AuthenticationError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var RateLimitError = class extends SendlyError {
|
|
72
|
+
/**
|
|
73
|
+
* Seconds to wait before retrying
|
|
74
|
+
*/
|
|
75
|
+
retryAfter;
|
|
76
|
+
constructor(message, retryAfter, statusCode, response) {
|
|
77
|
+
super(message, "rate_limit_exceeded", statusCode, response);
|
|
78
|
+
this.name = "RateLimitError";
|
|
79
|
+
this.retryAfter = retryAfter;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var InsufficientCreditsError = class extends SendlyError {
|
|
83
|
+
/**
|
|
84
|
+
* Credits needed for the operation
|
|
85
|
+
*/
|
|
86
|
+
creditsNeeded;
|
|
87
|
+
/**
|
|
88
|
+
* Current credit balance
|
|
89
|
+
*/
|
|
90
|
+
currentBalance;
|
|
91
|
+
constructor(message, creditsNeeded, currentBalance, statusCode, response) {
|
|
92
|
+
super(message, "insufficient_credits", statusCode, response);
|
|
93
|
+
this.name = "InsufficientCreditsError";
|
|
94
|
+
this.creditsNeeded = creditsNeeded;
|
|
95
|
+
this.currentBalance = currentBalance;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var ValidationError = class extends SendlyError {
|
|
99
|
+
constructor(message, code = "invalid_request", statusCode, response) {
|
|
100
|
+
super(message, code, statusCode, response);
|
|
101
|
+
this.name = "ValidationError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var NotFoundError = class extends SendlyError {
|
|
105
|
+
constructor(message, statusCode, response) {
|
|
106
|
+
super(message, "not_found", statusCode, response);
|
|
107
|
+
this.name = "NotFoundError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var NetworkError = class extends SendlyError {
|
|
111
|
+
constructor(message, cause) {
|
|
112
|
+
super(message, "internal_error");
|
|
113
|
+
this.name = "NetworkError";
|
|
114
|
+
this.cause = cause;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var TimeoutError = class extends SendlyError {
|
|
118
|
+
constructor(message = "Request timed out") {
|
|
119
|
+
super(message, "internal_error");
|
|
120
|
+
this.name = "TimeoutError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/utils/http.ts
|
|
125
|
+
var DEFAULT_BASE_URL = "https://sendly.live/api/";
|
|
126
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
127
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
128
|
+
var HttpClient = class {
|
|
129
|
+
config;
|
|
130
|
+
rateLimitInfo;
|
|
131
|
+
constructor(config) {
|
|
132
|
+
this.config = {
|
|
133
|
+
apiKey: config.apiKey,
|
|
134
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
135
|
+
timeout: config.timeout || DEFAULT_TIMEOUT,
|
|
136
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
137
|
+
};
|
|
138
|
+
if (!this.isValidApiKey(this.config.apiKey)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"Invalid API key format. Expected sk_test_v1_xxx or sk_live_v1_xxx"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const baseUrl = new URL(this.config.baseUrl);
|
|
144
|
+
if (baseUrl.protocol !== "https:" && !baseUrl.hostname.includes("localhost") && baseUrl.hostname !== "127.0.0.1") {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"API key must only be transmitted over HTTPS. Use https:// or localhost for development."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validate API key format
|
|
152
|
+
*/
|
|
153
|
+
isValidApiKey(key) {
|
|
154
|
+
return /^sk_(test|live)_v1_[a-zA-Z0-9_-]+$/.test(key);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get current rate limit info
|
|
158
|
+
*/
|
|
159
|
+
getRateLimitInfo() {
|
|
160
|
+
return this.rateLimitInfo;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if we're using a test key
|
|
164
|
+
*/
|
|
165
|
+
isTestMode() {
|
|
166
|
+
return this.config.apiKey.startsWith("sk_test_");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Make an HTTP request to the API
|
|
170
|
+
*/
|
|
171
|
+
async request(options) {
|
|
172
|
+
const url = this.buildUrl(options.path, options.query);
|
|
173
|
+
const headers = this.buildHeaders(options.headers);
|
|
174
|
+
let lastError;
|
|
175
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await this.executeRequest(url, {
|
|
178
|
+
method: options.method,
|
|
179
|
+
headers,
|
|
180
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
181
|
+
});
|
|
182
|
+
this.updateRateLimitInfo(response.headers);
|
|
183
|
+
const data = await this.parseResponse(response);
|
|
184
|
+
return data;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
lastError = error;
|
|
187
|
+
if (error instanceof SendlyError) {
|
|
188
|
+
if (error.statusCode === 401 || error.statusCode === 403) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
if (error.statusCode === 400 || error.statusCode === 404) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
if (error.statusCode === 402) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
if (error instanceof RateLimitError) {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (attempt < this.config.maxRetries) {
|
|
202
|
+
const backoffTime = this.calculateBackoff(attempt);
|
|
203
|
+
await this.sleep(backoffTime);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw lastError || new NetworkError("Request failed after retries");
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Execute the HTTP request
|
|
212
|
+
*/
|
|
213
|
+
async executeRequest(url, init) {
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(url, {
|
|
218
|
+
...init,
|
|
219
|
+
signal: controller.signal
|
|
220
|
+
});
|
|
221
|
+
return response;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.name === "AbortError") {
|
|
224
|
+
throw new TimeoutError(
|
|
225
|
+
`Request timed out after ${this.config.timeout}ms`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw new NetworkError(
|
|
229
|
+
`Network request failed: ${error.message}`,
|
|
230
|
+
error
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
clearTimeout(timeoutId);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Parse the response body
|
|
238
|
+
*/
|
|
239
|
+
async parseResponse(response) {
|
|
240
|
+
const contentType = response.headers.get("content-type");
|
|
241
|
+
let data;
|
|
242
|
+
if (contentType?.includes("application/json")) {
|
|
243
|
+
data = await response.json();
|
|
244
|
+
} else {
|
|
245
|
+
data = await response.text();
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
const errorResponse = data;
|
|
249
|
+
throw SendlyError.fromResponse(response.status, {
|
|
250
|
+
...errorResponse,
|
|
251
|
+
error: errorResponse?.error || "internal_error",
|
|
252
|
+
message: errorResponse?.message || `HTTP ${response.status}`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return data;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Build the full URL with query parameters
|
|
259
|
+
*/
|
|
260
|
+
buildUrl(path, query) {
|
|
261
|
+
const base = this.config.baseUrl.replace(/\/$/, "");
|
|
262
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
263
|
+
const fullUrl = `${base}/${cleanPath}`;
|
|
264
|
+
const url = new URL(fullUrl);
|
|
265
|
+
if (query) {
|
|
266
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
267
|
+
if (value !== void 0) {
|
|
268
|
+
url.searchParams.append(key, String(value));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return url.toString();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Build request headers
|
|
276
|
+
*/
|
|
277
|
+
buildHeaders(additionalHeaders) {
|
|
278
|
+
return {
|
|
279
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
Accept: "application/json",
|
|
282
|
+
"User-Agent": "@sendly/node/1.0.0",
|
|
283
|
+
...additionalHeaders
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Update rate limit info from response headers
|
|
288
|
+
*/
|
|
289
|
+
updateRateLimitInfo(headers) {
|
|
290
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
291
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
292
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
293
|
+
if (limit && remaining && reset) {
|
|
294
|
+
this.rateLimitInfo = {
|
|
295
|
+
limit: parseInt(limit, 10),
|
|
296
|
+
remaining: parseInt(remaining, 10),
|
|
297
|
+
reset: parseInt(reset, 10)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Calculate exponential backoff time
|
|
303
|
+
*/
|
|
304
|
+
calculateBackoff(attempt) {
|
|
305
|
+
const baseDelay = Math.pow(2, attempt) * 1e3;
|
|
306
|
+
const jitter = Math.random() * 500;
|
|
307
|
+
return Math.min(baseDelay + jitter, 3e4);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Sleep for a given number of milliseconds
|
|
311
|
+
*/
|
|
312
|
+
sleep(ms) {
|
|
313
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/types.ts
|
|
318
|
+
var CREDITS_PER_SMS = {
|
|
319
|
+
domestic: 1,
|
|
320
|
+
tier1: 8,
|
|
321
|
+
tier2: 12,
|
|
322
|
+
tier3: 16
|
|
323
|
+
};
|
|
324
|
+
var SUPPORTED_COUNTRIES = {
|
|
325
|
+
domestic: ["US", "CA"],
|
|
326
|
+
tier1: [
|
|
327
|
+
"GB",
|
|
328
|
+
"PL",
|
|
329
|
+
"PT",
|
|
330
|
+
"RO",
|
|
331
|
+
"CZ",
|
|
332
|
+
"HU",
|
|
333
|
+
"CN",
|
|
334
|
+
"KR",
|
|
335
|
+
"IN",
|
|
336
|
+
"PH",
|
|
337
|
+
"TH",
|
|
338
|
+
"VN"
|
|
339
|
+
],
|
|
340
|
+
tier2: [
|
|
341
|
+
"FR",
|
|
342
|
+
"ES",
|
|
343
|
+
"SE",
|
|
344
|
+
"NO",
|
|
345
|
+
"DK",
|
|
346
|
+
"FI",
|
|
347
|
+
"IE",
|
|
348
|
+
"JP",
|
|
349
|
+
"AU",
|
|
350
|
+
"NZ",
|
|
351
|
+
"SG",
|
|
352
|
+
"HK",
|
|
353
|
+
"MY",
|
|
354
|
+
"ID",
|
|
355
|
+
"BR",
|
|
356
|
+
"AR",
|
|
357
|
+
"CL",
|
|
358
|
+
"CO",
|
|
359
|
+
"ZA",
|
|
360
|
+
"GR"
|
|
361
|
+
],
|
|
362
|
+
tier3: [
|
|
363
|
+
"DE",
|
|
364
|
+
"IT",
|
|
365
|
+
"NL",
|
|
366
|
+
"BE",
|
|
367
|
+
"AT",
|
|
368
|
+
"CH",
|
|
369
|
+
"MX",
|
|
370
|
+
"IL",
|
|
371
|
+
"AE",
|
|
372
|
+
"SA",
|
|
373
|
+
"EG",
|
|
374
|
+
"NG",
|
|
375
|
+
"KE",
|
|
376
|
+
"TW",
|
|
377
|
+
"PK",
|
|
378
|
+
"TR"
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
var ALL_SUPPORTED_COUNTRIES = Object.values(SUPPORTED_COUNTRIES).flat();
|
|
382
|
+
var SANDBOX_TEST_NUMBERS = {
|
|
383
|
+
/** Always succeeds instantly */
|
|
384
|
+
SUCCESS: "+15550001234",
|
|
385
|
+
/** Succeeds after 10 second delay */
|
|
386
|
+
DELAYED: "+15550001010",
|
|
387
|
+
/** Fails with invalid_number error */
|
|
388
|
+
INVALID: "+15550001001",
|
|
389
|
+
/** Fails with carrier_rejected error after 2 seconds */
|
|
390
|
+
REJECTED: "+15550001002",
|
|
391
|
+
/** Fails with rate_limit_exceeded error */
|
|
392
|
+
RATE_LIMITED: "+15550001003"
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/utils/validation.ts
|
|
396
|
+
function validatePhoneNumber(phone) {
|
|
397
|
+
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
398
|
+
if (!phone) {
|
|
399
|
+
throw new ValidationError("Phone number is required");
|
|
400
|
+
}
|
|
401
|
+
if (!e164Regex.test(phone)) {
|
|
402
|
+
throw new ValidationError(
|
|
403
|
+
`Invalid phone number format: ${phone}. Expected E.164 format (e.g., +15551234567)`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function validateMessageText(text) {
|
|
408
|
+
if (!text) {
|
|
409
|
+
throw new ValidationError("Message text is required");
|
|
410
|
+
}
|
|
411
|
+
if (typeof text !== "string") {
|
|
412
|
+
throw new ValidationError("Message text must be a string");
|
|
413
|
+
}
|
|
414
|
+
if (text.length > 1600) {
|
|
415
|
+
console.warn(
|
|
416
|
+
`Message is ${text.length} characters. This will be split into ${Math.ceil(text.length / 160)} segments.`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function validateSenderId(from) {
|
|
421
|
+
if (!from) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (from.startsWith("+")) {
|
|
425
|
+
validatePhoneNumber(from);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const alphanumericRegex = /^[a-zA-Z0-9]{2,11}$/;
|
|
429
|
+
if (!alphanumericRegex.test(from)) {
|
|
430
|
+
throw new ValidationError(
|
|
431
|
+
`Invalid sender ID: ${from}. Must be 2-11 alphanumeric characters or a valid phone number.`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function validateLimit(limit) {
|
|
436
|
+
if (limit === void 0) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (typeof limit !== "number" || !Number.isInteger(limit)) {
|
|
440
|
+
throw new ValidationError("Limit must be an integer");
|
|
441
|
+
}
|
|
442
|
+
if (limit < 1 || limit > 100) {
|
|
443
|
+
throw new ValidationError("Limit must be between 1 and 100");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function validateMessageId(id) {
|
|
447
|
+
if (!id) {
|
|
448
|
+
throw new ValidationError("Message ID is required");
|
|
449
|
+
}
|
|
450
|
+
if (typeof id !== "string") {
|
|
451
|
+
throw new ValidationError("Message ID must be a string");
|
|
452
|
+
}
|
|
453
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
454
|
+
const prefixedRegex = /^(msg|schd|batch)_[a-zA-Z0-9]+$/;
|
|
455
|
+
if (!uuidRegex.test(id) && !prefixedRegex.test(id)) {
|
|
456
|
+
throw new ValidationError(`Invalid message ID format: ${id}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function getCountryFromPhone(phone) {
|
|
460
|
+
const digits = phone.replace(/^\+/, "");
|
|
461
|
+
if (digits.startsWith("1") && digits.length === 11) {
|
|
462
|
+
return "US";
|
|
463
|
+
}
|
|
464
|
+
const countryPrefixes = {
|
|
465
|
+
"44": "GB",
|
|
466
|
+
"48": "PL",
|
|
467
|
+
"351": "PT",
|
|
468
|
+
"40": "RO",
|
|
469
|
+
"420": "CZ",
|
|
470
|
+
"36": "HU",
|
|
471
|
+
"86": "CN",
|
|
472
|
+
"82": "KR",
|
|
473
|
+
"91": "IN",
|
|
474
|
+
"63": "PH",
|
|
475
|
+
"66": "TH",
|
|
476
|
+
"84": "VN",
|
|
477
|
+
"33": "FR",
|
|
478
|
+
"34": "ES",
|
|
479
|
+
"46": "SE",
|
|
480
|
+
"47": "NO",
|
|
481
|
+
"45": "DK",
|
|
482
|
+
"358": "FI",
|
|
483
|
+
"353": "IE",
|
|
484
|
+
"81": "JP",
|
|
485
|
+
"61": "AU",
|
|
486
|
+
"64": "NZ",
|
|
487
|
+
"65": "SG",
|
|
488
|
+
"852": "HK",
|
|
489
|
+
"60": "MY",
|
|
490
|
+
"62": "ID",
|
|
491
|
+
"55": "BR",
|
|
492
|
+
"54": "AR",
|
|
493
|
+
"56": "CL",
|
|
494
|
+
"57": "CO",
|
|
495
|
+
"27": "ZA",
|
|
496
|
+
"30": "GR",
|
|
497
|
+
"49": "DE",
|
|
498
|
+
"39": "IT",
|
|
499
|
+
"31": "NL",
|
|
500
|
+
"32": "BE",
|
|
501
|
+
"43": "AT",
|
|
502
|
+
"41": "CH",
|
|
503
|
+
"52": "MX",
|
|
504
|
+
"972": "IL",
|
|
505
|
+
"971": "AE",
|
|
506
|
+
"966": "SA",
|
|
507
|
+
"20": "EG",
|
|
508
|
+
"234": "NG",
|
|
509
|
+
"254": "KE",
|
|
510
|
+
"886": "TW",
|
|
511
|
+
"92": "PK",
|
|
512
|
+
"90": "TR"
|
|
513
|
+
};
|
|
514
|
+
const sortedPrefixes = Object.keys(countryPrefixes).sort(
|
|
515
|
+
(a, b) => b.length - a.length
|
|
516
|
+
);
|
|
517
|
+
for (const prefix of sortedPrefixes) {
|
|
518
|
+
if (digits.startsWith(prefix)) {
|
|
519
|
+
return countryPrefixes[prefix];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
function isCountrySupported(countryCode) {
|
|
525
|
+
return ALL_SUPPORTED_COUNTRIES.includes(countryCode.toUpperCase());
|
|
526
|
+
}
|
|
527
|
+
function calculateSegments(text) {
|
|
528
|
+
const isUnicode = /[^\x00-\x7F]/.test(text);
|
|
529
|
+
const singleLimit = isUnicode ? 70 : 160;
|
|
530
|
+
const multiLimit = isUnicode ? 67 : 153;
|
|
531
|
+
if (text.length <= singleLimit) {
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
return Math.ceil(text.length / multiLimit);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/resources/messages.ts
|
|
538
|
+
var MessagesResource = class {
|
|
539
|
+
http;
|
|
540
|
+
constructor(http) {
|
|
541
|
+
this.http = http;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Send an SMS message
|
|
545
|
+
*
|
|
546
|
+
* @param request - Message details
|
|
547
|
+
* @returns The created message
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```typescript
|
|
551
|
+
* const message = await sendly.messages.send({
|
|
552
|
+
* to: '+15551234567',
|
|
553
|
+
* text: 'Your verification code is: 123456'
|
|
554
|
+
* });
|
|
555
|
+
*
|
|
556
|
+
* console.log(message.id); // msg_xxx
|
|
557
|
+
* console.log(message.status); // 'queued'
|
|
558
|
+
* console.log(message.segments); // 1
|
|
559
|
+
* ```
|
|
560
|
+
*
|
|
561
|
+
* @throws {ValidationError} If the request is invalid
|
|
562
|
+
* @throws {InsufficientCreditsError} If credit balance is too low
|
|
563
|
+
* @throws {AuthenticationError} If the API key is invalid
|
|
564
|
+
* @throws {RateLimitError} If rate limit is exceeded
|
|
565
|
+
*/
|
|
566
|
+
async send(request) {
|
|
567
|
+
validatePhoneNumber(request.to);
|
|
568
|
+
validateMessageText(request.text);
|
|
569
|
+
if (request.from) {
|
|
570
|
+
validateSenderId(request.from);
|
|
571
|
+
}
|
|
572
|
+
const message = await this.http.request({
|
|
573
|
+
method: "POST",
|
|
574
|
+
path: "/v1/messages",
|
|
575
|
+
body: {
|
|
576
|
+
to: request.to,
|
|
577
|
+
text: request.text,
|
|
578
|
+
...request.from && { from: request.from }
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
return message;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* List sent messages
|
|
585
|
+
*
|
|
586
|
+
* @param options - List options
|
|
587
|
+
* @returns Paginated list of messages
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```typescript
|
|
591
|
+
* // Get last 50 messages (default)
|
|
592
|
+
* const { data: messages, count } = await sendly.messages.list();
|
|
593
|
+
*
|
|
594
|
+
* // Get last 10 messages
|
|
595
|
+
* const { data: messages } = await sendly.messages.list({ limit: 10 });
|
|
596
|
+
*
|
|
597
|
+
* // Iterate through messages
|
|
598
|
+
* for (const msg of messages) {
|
|
599
|
+
* console.log(`${msg.to}: ${msg.status}`);
|
|
600
|
+
* }
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* @throws {AuthenticationError} If the API key is invalid
|
|
604
|
+
* @throws {RateLimitError} If rate limit is exceeded
|
|
605
|
+
*/
|
|
606
|
+
async list(options = {}) {
|
|
607
|
+
validateLimit(options.limit);
|
|
608
|
+
const response = await this.http.request({
|
|
609
|
+
method: "GET",
|
|
610
|
+
path: "/v1/messages",
|
|
611
|
+
query: {
|
|
612
|
+
limit: options.limit,
|
|
613
|
+
offset: options.offset,
|
|
614
|
+
status: options.status
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return response;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Get a specific message by ID
|
|
621
|
+
*
|
|
622
|
+
* @param id - Message ID
|
|
623
|
+
* @returns The message details
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* const message = await sendly.messages.get('msg_xxx');
|
|
628
|
+
*
|
|
629
|
+
* console.log(message.status); // 'delivered'
|
|
630
|
+
* console.log(message.deliveredAt); // '2025-01-15T10:30:00Z'
|
|
631
|
+
* ```
|
|
632
|
+
*
|
|
633
|
+
* @throws {NotFoundError} If the message doesn't exist
|
|
634
|
+
* @throws {AuthenticationError} If the API key is invalid
|
|
635
|
+
* @throws {RateLimitError} If rate limit is exceeded
|
|
636
|
+
*/
|
|
637
|
+
async get(id) {
|
|
638
|
+
validateMessageId(id);
|
|
639
|
+
const message = await this.http.request({
|
|
640
|
+
method: "GET",
|
|
641
|
+
path: `/v1/messages/${encodeURIComponent(id)}`
|
|
642
|
+
});
|
|
643
|
+
return message;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Iterate through all messages with automatic pagination
|
|
647
|
+
*
|
|
648
|
+
* @param options - List options (limit is used as batch size)
|
|
649
|
+
* @yields Message objects one at a time
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* ```typescript
|
|
653
|
+
* // Iterate through all messages
|
|
654
|
+
* for await (const message of sendly.messages.listAll()) {
|
|
655
|
+
* console.log(`${message.id}: ${message.status}`);
|
|
656
|
+
* }
|
|
657
|
+
*
|
|
658
|
+
* // With custom batch size
|
|
659
|
+
* for await (const message of sendly.messages.listAll({ limit: 100 })) {
|
|
660
|
+
* console.log(message.to);
|
|
661
|
+
* }
|
|
662
|
+
* ```
|
|
663
|
+
*
|
|
664
|
+
* @throws {AuthenticationError} If the API key is invalid
|
|
665
|
+
* @throws {RateLimitError} If rate limit is exceeded
|
|
666
|
+
*/
|
|
667
|
+
async *listAll(options = {}) {
|
|
668
|
+
const batchSize = Math.min(options.limit || 100, 100);
|
|
669
|
+
let offset = 0;
|
|
670
|
+
let hasMore = true;
|
|
671
|
+
while (hasMore) {
|
|
672
|
+
const response = await this.http.request({
|
|
673
|
+
method: "GET",
|
|
674
|
+
path: "/v1/messages",
|
|
675
|
+
query: {
|
|
676
|
+
limit: batchSize,
|
|
677
|
+
offset
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
for (const message of response.data) {
|
|
681
|
+
yield message;
|
|
682
|
+
}
|
|
683
|
+
if (response.data.length < batchSize) {
|
|
684
|
+
hasMore = false;
|
|
685
|
+
} else {
|
|
686
|
+
offset += batchSize;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// ==========================================================================
|
|
691
|
+
// Scheduled Messages
|
|
692
|
+
// ==========================================================================
|
|
693
|
+
/**
|
|
694
|
+
* Schedule an SMS message for future delivery
|
|
695
|
+
*
|
|
696
|
+
* @param request - Schedule request details
|
|
697
|
+
* @returns The scheduled message
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```typescript
|
|
701
|
+
* const scheduled = await sendly.messages.schedule({
|
|
702
|
+
* to: '+15551234567',
|
|
703
|
+
* text: 'Your appointment reminder!',
|
|
704
|
+
* scheduledAt: '2025-01-20T10:00:00Z'
|
|
705
|
+
* });
|
|
706
|
+
*
|
|
707
|
+
* console.log(scheduled.id); // msg_xxx
|
|
708
|
+
* console.log(scheduled.status); // 'scheduled'
|
|
709
|
+
* console.log(scheduled.scheduledAt); // '2025-01-20T10:00:00Z'
|
|
710
|
+
* ```
|
|
711
|
+
*
|
|
712
|
+
* @throws {ValidationError} If the request is invalid
|
|
713
|
+
* @throws {InsufficientCreditsError} If credit balance is too low
|
|
714
|
+
* @throws {AuthenticationError} If the API key is invalid
|
|
715
|
+
*/
|
|
716
|
+
async schedule(request) {
|
|
717
|
+
validatePhoneNumber(request.to);
|
|
718
|
+
validateMessageText(request.text);
|
|
719
|
+
if (request.from) {
|
|
720
|
+
validateSenderId(request.from);
|
|
721
|
+
}
|
|
722
|
+
const scheduledTime = new Date(request.scheduledAt);
|
|
723
|
+
const now = /* @__PURE__ */ new Date();
|
|
724
|
+
const oneMinuteFromNow = new Date(now.getTime() + 60 * 1e3);
|
|
725
|
+
if (isNaN(scheduledTime.getTime())) {
|
|
726
|
+
throw new Error("Invalid scheduledAt format. Use ISO 8601 format.");
|
|
727
|
+
}
|
|
728
|
+
if (scheduledTime <= oneMinuteFromNow) {
|
|
729
|
+
throw new Error("scheduledAt must be at least 1 minute in the future.");
|
|
730
|
+
}
|
|
731
|
+
const scheduled = await this.http.request({
|
|
732
|
+
method: "POST",
|
|
733
|
+
path: "/v1/messages/schedule",
|
|
734
|
+
body: {
|
|
735
|
+
to: request.to,
|
|
736
|
+
text: request.text,
|
|
737
|
+
scheduledAt: request.scheduledAt,
|
|
738
|
+
...request.from && { from: request.from }
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
return scheduled;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* List scheduled messages
|
|
745
|
+
*
|
|
746
|
+
* @param options - List options
|
|
747
|
+
* @returns Paginated list of scheduled messages
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* const { data: scheduled } = await sendly.messages.listScheduled();
|
|
752
|
+
*
|
|
753
|
+
* for (const msg of scheduled) {
|
|
754
|
+
* console.log(`${msg.to}: ${msg.scheduledAt}`);
|
|
755
|
+
* }
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
async listScheduled(options = {}) {
|
|
759
|
+
validateLimit(options.limit);
|
|
760
|
+
const response = await this.http.request({
|
|
761
|
+
method: "GET",
|
|
762
|
+
path: "/v1/messages/scheduled",
|
|
763
|
+
query: {
|
|
764
|
+
limit: options.limit,
|
|
765
|
+
offset: options.offset,
|
|
766
|
+
status: options.status
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
return response;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get a specific scheduled message by ID
|
|
773
|
+
*
|
|
774
|
+
* @param id - Message ID
|
|
775
|
+
* @returns The scheduled message details
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```typescript
|
|
779
|
+
* const scheduled = await sendly.messages.getScheduled('msg_xxx');
|
|
780
|
+
* console.log(scheduled.scheduledAt);
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
async getScheduled(id) {
|
|
784
|
+
validateMessageId(id);
|
|
785
|
+
const scheduled = await this.http.request({
|
|
786
|
+
method: "GET",
|
|
787
|
+
path: `/v1/messages/scheduled/${encodeURIComponent(id)}`
|
|
788
|
+
});
|
|
789
|
+
return scheduled;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Cancel a scheduled message
|
|
793
|
+
*
|
|
794
|
+
* @param id - Message ID to cancel
|
|
795
|
+
* @returns Cancellation confirmation with refunded credits
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* ```typescript
|
|
799
|
+
* const result = await sendly.messages.cancelScheduled('msg_xxx');
|
|
800
|
+
*
|
|
801
|
+
* console.log(result.status); // 'cancelled'
|
|
802
|
+
* console.log(result.creditsRefunded); // 1
|
|
803
|
+
* ```
|
|
804
|
+
*
|
|
805
|
+
* @throws {NotFoundError} If the message doesn't exist
|
|
806
|
+
* @throws {ValidationError} If the message is not cancellable
|
|
807
|
+
*/
|
|
808
|
+
async cancelScheduled(id) {
|
|
809
|
+
validateMessageId(id);
|
|
810
|
+
const result = await this.http.request({
|
|
811
|
+
method: "DELETE",
|
|
812
|
+
path: `/v1/messages/scheduled/${encodeURIComponent(id)}`
|
|
813
|
+
});
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
// ==========================================================================
|
|
817
|
+
// Batch Messages
|
|
818
|
+
// ==========================================================================
|
|
819
|
+
/**
|
|
820
|
+
* Send multiple SMS messages in a single batch
|
|
821
|
+
*
|
|
822
|
+
* @param request - Batch request with array of messages
|
|
823
|
+
* @returns Batch response with individual message results
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```typescript
|
|
827
|
+
* const batch = await sendly.messages.sendBatch({
|
|
828
|
+
* messages: [
|
|
829
|
+
* { to: '+15551234567', text: 'Hello User 1!' },
|
|
830
|
+
* { to: '+15559876543', text: 'Hello User 2!' }
|
|
831
|
+
* ]
|
|
832
|
+
* });
|
|
833
|
+
*
|
|
834
|
+
* console.log(batch.batchId); // batch_xxx
|
|
835
|
+
* console.log(batch.queued); // 2
|
|
836
|
+
* console.log(batch.creditsUsed); // 2
|
|
837
|
+
* ```
|
|
838
|
+
*
|
|
839
|
+
* @throws {ValidationError} If any message is invalid
|
|
840
|
+
* @throws {InsufficientCreditsError} If credit balance is too low
|
|
841
|
+
*/
|
|
842
|
+
async sendBatch(request) {
|
|
843
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
844
|
+
throw new Error("messages must be a non-empty array");
|
|
845
|
+
}
|
|
846
|
+
if (request.messages.length > 1e3) {
|
|
847
|
+
throw new Error("Maximum 1000 messages per batch");
|
|
848
|
+
}
|
|
849
|
+
for (const msg of request.messages) {
|
|
850
|
+
validatePhoneNumber(msg.to);
|
|
851
|
+
validateMessageText(msg.text);
|
|
852
|
+
}
|
|
853
|
+
if (request.from) {
|
|
854
|
+
validateSenderId(request.from);
|
|
855
|
+
}
|
|
856
|
+
const batch = await this.http.request({
|
|
857
|
+
method: "POST",
|
|
858
|
+
path: "/v1/messages/batch",
|
|
859
|
+
body: {
|
|
860
|
+
messages: request.messages,
|
|
861
|
+
...request.from && { from: request.from }
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
return batch;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get batch status and results
|
|
868
|
+
*
|
|
869
|
+
* @param batchId - Batch ID
|
|
870
|
+
* @returns Batch details with message results
|
|
871
|
+
*
|
|
872
|
+
* @example
|
|
873
|
+
* ```typescript
|
|
874
|
+
* const batch = await sendly.messages.getBatch('batch_xxx');
|
|
875
|
+
*
|
|
876
|
+
* console.log(batch.status); // 'completed'
|
|
877
|
+
* console.log(batch.sent); // 2
|
|
878
|
+
* console.log(batch.failed); // 0
|
|
879
|
+
* ```
|
|
880
|
+
*/
|
|
881
|
+
async getBatch(batchId) {
|
|
882
|
+
if (!batchId || !batchId.startsWith("batch_")) {
|
|
883
|
+
throw new Error("Invalid batch ID format");
|
|
884
|
+
}
|
|
885
|
+
const batch = await this.http.request({
|
|
886
|
+
method: "GET",
|
|
887
|
+
path: `/v1/messages/batch/${encodeURIComponent(batchId)}`
|
|
888
|
+
});
|
|
889
|
+
return batch;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* List message batches
|
|
893
|
+
*
|
|
894
|
+
* @param options - List options
|
|
895
|
+
* @returns Paginated list of batches
|
|
896
|
+
*
|
|
897
|
+
* @example
|
|
898
|
+
* ```typescript
|
|
899
|
+
* const { data: batches } = await sendly.messages.listBatches();
|
|
900
|
+
*
|
|
901
|
+
* for (const batch of batches) {
|
|
902
|
+
* console.log(`${batch.batchId}: ${batch.status}`);
|
|
903
|
+
* }
|
|
904
|
+
* ```
|
|
905
|
+
*/
|
|
906
|
+
async listBatches(options = {}) {
|
|
907
|
+
validateLimit(options.limit);
|
|
908
|
+
const response = await this.http.request({
|
|
909
|
+
method: "GET",
|
|
910
|
+
path: "/v1/messages/batches",
|
|
911
|
+
query: {
|
|
912
|
+
limit: options.limit,
|
|
913
|
+
offset: options.offset,
|
|
914
|
+
status: options.status
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
return response;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/client.ts
|
|
922
|
+
var DEFAULT_BASE_URL2 = "https://sendly.live/api";
|
|
923
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
924
|
+
var DEFAULT_MAX_RETRIES2 = 3;
|
|
925
|
+
var Sendly = class {
|
|
926
|
+
/**
|
|
927
|
+
* Messages API resource
|
|
928
|
+
*
|
|
929
|
+
* @example
|
|
930
|
+
* ```typescript
|
|
931
|
+
* // Send a message
|
|
932
|
+
* await sendly.messages.send({ to: '+1555...', text: 'Hello!' });
|
|
933
|
+
*
|
|
934
|
+
* // List messages
|
|
935
|
+
* const { data } = await sendly.messages.list({ limit: 10 });
|
|
936
|
+
*
|
|
937
|
+
* // Get a message
|
|
938
|
+
* const msg = await sendly.messages.get('msg_xxx');
|
|
939
|
+
* ```
|
|
940
|
+
*/
|
|
941
|
+
messages;
|
|
942
|
+
http;
|
|
943
|
+
config;
|
|
944
|
+
/**
|
|
945
|
+
* Create a new Sendly client
|
|
946
|
+
*
|
|
947
|
+
* @param configOrApiKey - API key string or configuration object
|
|
948
|
+
*/
|
|
949
|
+
constructor(configOrApiKey) {
|
|
950
|
+
if (typeof configOrApiKey === "string") {
|
|
951
|
+
this.config = {
|
|
952
|
+
apiKey: configOrApiKey,
|
|
953
|
+
baseUrl: DEFAULT_BASE_URL2,
|
|
954
|
+
timeout: DEFAULT_TIMEOUT2,
|
|
955
|
+
maxRetries: DEFAULT_MAX_RETRIES2
|
|
956
|
+
};
|
|
957
|
+
} else {
|
|
958
|
+
this.config = {
|
|
959
|
+
apiKey: configOrApiKey.apiKey,
|
|
960
|
+
baseUrl: configOrApiKey.baseUrl || DEFAULT_BASE_URL2,
|
|
961
|
+
timeout: configOrApiKey.timeout || DEFAULT_TIMEOUT2,
|
|
962
|
+
maxRetries: configOrApiKey.maxRetries ?? DEFAULT_MAX_RETRIES2
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
this.http = new HttpClient({
|
|
966
|
+
apiKey: this.config.apiKey,
|
|
967
|
+
baseUrl: this.config.baseUrl,
|
|
968
|
+
timeout: this.config.timeout,
|
|
969
|
+
maxRetries: this.config.maxRetries
|
|
970
|
+
});
|
|
971
|
+
this.messages = new MessagesResource(this.http);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Check if the client is using a test API key
|
|
975
|
+
*
|
|
976
|
+
* @returns true if using a test key (sk_test_v1_xxx)
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* ```typescript
|
|
980
|
+
* if (sendly.isTestMode()) {
|
|
981
|
+
* console.log('Running in test mode');
|
|
982
|
+
* }
|
|
983
|
+
* ```
|
|
984
|
+
*/
|
|
985
|
+
isTestMode() {
|
|
986
|
+
return this.http.isTestMode();
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get current rate limit information
|
|
990
|
+
*
|
|
991
|
+
* Returns the rate limit info from the most recent API request.
|
|
992
|
+
*
|
|
993
|
+
* @returns Rate limit info or undefined if no requests have been made
|
|
994
|
+
*
|
|
995
|
+
* @example
|
|
996
|
+
* ```typescript
|
|
997
|
+
* await sendly.messages.send({ to: '+1555...', text: 'Hello!' });
|
|
998
|
+
*
|
|
999
|
+
* const rateLimit = sendly.getRateLimitInfo();
|
|
1000
|
+
* if (rateLimit) {
|
|
1001
|
+
* console.log(`${rateLimit.remaining}/${rateLimit.limit} requests remaining`);
|
|
1002
|
+
* console.log(`Resets in ${rateLimit.reset} seconds`);
|
|
1003
|
+
* }
|
|
1004
|
+
* ```
|
|
1005
|
+
*/
|
|
1006
|
+
getRateLimitInfo() {
|
|
1007
|
+
return this.http.getRateLimitInfo();
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get the configured base URL
|
|
1011
|
+
*/
|
|
1012
|
+
getBaseUrl() {
|
|
1013
|
+
return this.config.baseUrl;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// src/utils/webhooks.ts
|
|
1018
|
+
import * as crypto from "crypto";
|
|
1019
|
+
var WebhookSignatureError = class extends Error {
|
|
1020
|
+
constructor(message = "Invalid webhook signature") {
|
|
1021
|
+
super(message);
|
|
1022
|
+
this.name = "WebhookSignatureError";
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
1026
|
+
if (!payload || !signature || !secret) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
const expectedSignature = generateWebhookSignature(payload, secret);
|
|
1030
|
+
try {
|
|
1031
|
+
return crypto.timingSafeEqual(
|
|
1032
|
+
Buffer.from(signature),
|
|
1033
|
+
Buffer.from(expectedSignature)
|
|
1034
|
+
);
|
|
1035
|
+
} catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function parseWebhookEvent(payload, signature, secret) {
|
|
1040
|
+
if (!verifyWebhookSignature(payload, signature, secret)) {
|
|
1041
|
+
throw new WebhookSignatureError();
|
|
1042
|
+
}
|
|
1043
|
+
let event;
|
|
1044
|
+
try {
|
|
1045
|
+
event = JSON.parse(payload);
|
|
1046
|
+
} catch {
|
|
1047
|
+
throw new Error("Failed to parse webhook payload");
|
|
1048
|
+
}
|
|
1049
|
+
if (!event.id || !event.type || !event.createdAt) {
|
|
1050
|
+
throw new Error("Invalid webhook event structure");
|
|
1051
|
+
}
|
|
1052
|
+
return event;
|
|
1053
|
+
}
|
|
1054
|
+
function generateWebhookSignature(payload, secret) {
|
|
1055
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
1056
|
+
hmac.update(payload);
|
|
1057
|
+
return "sha256=" + hmac.digest("hex");
|
|
1058
|
+
}
|
|
1059
|
+
var Webhooks = class {
|
|
1060
|
+
secret;
|
|
1061
|
+
/**
|
|
1062
|
+
* Create a new Webhooks instance
|
|
1063
|
+
* @param secret - Your webhook secret from the Sendly dashboard
|
|
1064
|
+
*/
|
|
1065
|
+
constructor(secret) {
|
|
1066
|
+
if (!secret) {
|
|
1067
|
+
throw new Error("Webhook secret is required");
|
|
1068
|
+
}
|
|
1069
|
+
this.secret = secret;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Verify a webhook signature
|
|
1073
|
+
* @param payload - Raw request body
|
|
1074
|
+
* @param signature - X-Sendly-Signature header
|
|
1075
|
+
*/
|
|
1076
|
+
verify(payload, signature) {
|
|
1077
|
+
return verifyWebhookSignature(payload, signature, this.secret);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Parse and verify a webhook event
|
|
1081
|
+
* @param payload - Raw request body
|
|
1082
|
+
* @param signature - X-Sendly-Signature header
|
|
1083
|
+
*/
|
|
1084
|
+
parse(payload, signature) {
|
|
1085
|
+
return parseWebhookEvent(payload, signature, this.secret);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Generate a signature for testing
|
|
1089
|
+
* @param payload - Payload to sign
|
|
1090
|
+
*/
|
|
1091
|
+
sign(payload) {
|
|
1092
|
+
return generateWebhookSignature(payload, this.secret);
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
export {
|
|
1096
|
+
ALL_SUPPORTED_COUNTRIES,
|
|
1097
|
+
AuthenticationError,
|
|
1098
|
+
CREDITS_PER_SMS,
|
|
1099
|
+
InsufficientCreditsError,
|
|
1100
|
+
NetworkError,
|
|
1101
|
+
NotFoundError,
|
|
1102
|
+
RateLimitError,
|
|
1103
|
+
SANDBOX_TEST_NUMBERS,
|
|
1104
|
+
SUPPORTED_COUNTRIES,
|
|
1105
|
+
Sendly,
|
|
1106
|
+
SendlyError,
|
|
1107
|
+
TimeoutError,
|
|
1108
|
+
ValidationError,
|
|
1109
|
+
WebhookSignatureError,
|
|
1110
|
+
Webhooks,
|
|
1111
|
+
calculateSegments,
|
|
1112
|
+
Sendly as default,
|
|
1113
|
+
generateWebhookSignature,
|
|
1114
|
+
getCountryFromPhone,
|
|
1115
|
+
isCountrySupported,
|
|
1116
|
+
parseWebhookEvent,
|
|
1117
|
+
validateMessageText,
|
|
1118
|
+
validatePhoneNumber,
|
|
1119
|
+
validateSenderId,
|
|
1120
|
+
verifyWebhookSignature
|
|
1121
|
+
};
|