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