@sendly/node 1.0.8 → 1.1.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 CHANGED
@@ -978,6 +978,443 @@ var MessagesResource = class {
978
978
  }
979
979
  };
980
980
 
981
+ // src/utils/transform.ts
982
+ function snakeToCamel(str) {
983
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
984
+ }
985
+ function transformKeys(obj) {
986
+ if (obj === null || obj === void 0) {
987
+ return obj;
988
+ }
989
+ if (Array.isArray(obj)) {
990
+ return obj.map((item) => transformKeys(item));
991
+ }
992
+ if (typeof obj === "object") {
993
+ const result = {};
994
+ for (const [key, value] of Object.entries(obj)) {
995
+ const camelKey = snakeToCamel(key);
996
+ result[camelKey] = transformKeys(value);
997
+ }
998
+ return result;
999
+ }
1000
+ return obj;
1001
+ }
1002
+
1003
+ // src/resources/webhooks.ts
1004
+ var WebhooksResource = class {
1005
+ http;
1006
+ constructor(http) {
1007
+ this.http = http;
1008
+ }
1009
+ /**
1010
+ * Create a new webhook endpoint
1011
+ *
1012
+ * @param options - Webhook configuration
1013
+ * @returns The created webhook with signing secret (shown only once!)
1014
+ *
1015
+ * @example
1016
+ * ```typescript
1017
+ * const webhook = await sendly.webhooks.create({
1018
+ * url: 'https://example.com/webhooks/sendly',
1019
+ * events: ['message.delivered', 'message.failed'],
1020
+ * description: 'Production webhook'
1021
+ * });
1022
+ *
1023
+ * // IMPORTANT: Save this secret securely - it's only shown once!
1024
+ * console.log('Webhook secret:', webhook.secret);
1025
+ * ```
1026
+ *
1027
+ * @throws {ValidationError} If the URL is invalid or events are empty
1028
+ * @throws {AuthenticationError} If the API key is invalid
1029
+ */
1030
+ async create(options) {
1031
+ if (!options.url || !options.url.startsWith("https://")) {
1032
+ throw new Error("Webhook URL must be HTTPS");
1033
+ }
1034
+ if (!options.events || options.events.length === 0) {
1035
+ throw new Error("At least one event type is required");
1036
+ }
1037
+ const response = await this.http.request({
1038
+ method: "POST",
1039
+ path: "/webhooks",
1040
+ body: {
1041
+ url: options.url,
1042
+ events: options.events,
1043
+ ...options.description && { description: options.description },
1044
+ ...options.metadata && { metadata: options.metadata }
1045
+ }
1046
+ });
1047
+ return transformKeys(response);
1048
+ }
1049
+ /**
1050
+ * List all webhooks
1051
+ *
1052
+ * @returns Array of webhook configurations
1053
+ *
1054
+ * @example
1055
+ * ```typescript
1056
+ * const webhooks = await sendly.webhooks.list();
1057
+ *
1058
+ * for (const webhook of webhooks) {
1059
+ * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`);
1060
+ * }
1061
+ * ```
1062
+ */
1063
+ async list() {
1064
+ const response = await this.http.request({
1065
+ method: "GET",
1066
+ path: "/webhooks"
1067
+ });
1068
+ return response.map((item) => transformKeys(item));
1069
+ }
1070
+ /**
1071
+ * Get a specific webhook by ID
1072
+ *
1073
+ * @param id - Webhook ID (whk_xxx)
1074
+ * @returns The webhook details
1075
+ *
1076
+ * @example
1077
+ * ```typescript
1078
+ * const webhook = await sendly.webhooks.get('whk_xxx');
1079
+ * console.log(`Success rate: ${webhook.successRate}%`);
1080
+ * ```
1081
+ *
1082
+ * @throws {NotFoundError} If the webhook doesn't exist
1083
+ */
1084
+ async get(id) {
1085
+ if (!id || !id.startsWith("whk_")) {
1086
+ throw new Error("Invalid webhook ID format");
1087
+ }
1088
+ const response = await this.http.request({
1089
+ method: "GET",
1090
+ path: `/webhooks/${encodeURIComponent(id)}`
1091
+ });
1092
+ return transformKeys(response);
1093
+ }
1094
+ /**
1095
+ * Update a webhook configuration
1096
+ *
1097
+ * @param id - Webhook ID
1098
+ * @param options - Fields to update
1099
+ * @returns The updated webhook
1100
+ *
1101
+ * @example
1102
+ * ```typescript
1103
+ * // Update URL
1104
+ * await sendly.webhooks.update('whk_xxx', {
1105
+ * url: 'https://new-endpoint.example.com/webhooks'
1106
+ * });
1107
+ *
1108
+ * // Disable webhook
1109
+ * await sendly.webhooks.update('whk_xxx', { isActive: false });
1110
+ *
1111
+ * // Change event subscriptions
1112
+ * await sendly.webhooks.update('whk_xxx', {
1113
+ * events: ['message.delivered']
1114
+ * });
1115
+ * ```
1116
+ */
1117
+ async update(id, options) {
1118
+ if (!id || !id.startsWith("whk_")) {
1119
+ throw new Error("Invalid webhook ID format");
1120
+ }
1121
+ if (options.url && !options.url.startsWith("https://")) {
1122
+ throw new Error("Webhook URL must be HTTPS");
1123
+ }
1124
+ const response = await this.http.request({
1125
+ method: "PATCH",
1126
+ path: `/webhooks/${encodeURIComponent(id)}`,
1127
+ body: {
1128
+ ...options.url !== void 0 && { url: options.url },
1129
+ ...options.events !== void 0 && { events: options.events },
1130
+ ...options.description !== void 0 && {
1131
+ description: options.description
1132
+ },
1133
+ ...options.isActive !== void 0 && { is_active: options.isActive },
1134
+ ...options.metadata !== void 0 && { metadata: options.metadata }
1135
+ }
1136
+ });
1137
+ return transformKeys(response);
1138
+ }
1139
+ /**
1140
+ * Delete a webhook
1141
+ *
1142
+ * @param id - Webhook ID
1143
+ *
1144
+ * @example
1145
+ * ```typescript
1146
+ * await sendly.webhooks.delete('whk_xxx');
1147
+ * ```
1148
+ *
1149
+ * @throws {NotFoundError} If the webhook doesn't exist
1150
+ */
1151
+ async delete(id) {
1152
+ if (!id || !id.startsWith("whk_")) {
1153
+ throw new Error("Invalid webhook ID format");
1154
+ }
1155
+ await this.http.request({
1156
+ method: "DELETE",
1157
+ path: `/webhooks/${encodeURIComponent(id)}`
1158
+ });
1159
+ }
1160
+ /**
1161
+ * Send a test event to a webhook endpoint
1162
+ *
1163
+ * @param id - Webhook ID
1164
+ * @returns Test result with response details
1165
+ *
1166
+ * @example
1167
+ * ```typescript
1168
+ * const result = await sendly.webhooks.test('whk_xxx');
1169
+ *
1170
+ * if (result.success) {
1171
+ * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`);
1172
+ * } else {
1173
+ * console.log(`Test failed: ${result.error}`);
1174
+ * }
1175
+ * ```
1176
+ */
1177
+ async test(id) {
1178
+ if (!id || !id.startsWith("whk_")) {
1179
+ throw new Error("Invalid webhook ID format");
1180
+ }
1181
+ const response = await this.http.request({
1182
+ method: "POST",
1183
+ path: `/webhooks/${encodeURIComponent(id)}/test`
1184
+ });
1185
+ return transformKeys(response);
1186
+ }
1187
+ /**
1188
+ * Rotate the webhook signing secret
1189
+ *
1190
+ * The old secret remains valid for 24 hours to allow for graceful migration.
1191
+ *
1192
+ * @param id - Webhook ID
1193
+ * @returns New secret and expiration info
1194
+ *
1195
+ * @example
1196
+ * ```typescript
1197
+ * const rotation = await sendly.webhooks.rotateSecret('whk_xxx');
1198
+ *
1199
+ * // Update your webhook handler with the new secret
1200
+ * console.log('New secret:', rotation.newSecret);
1201
+ * console.log('Old secret expires:', rotation.oldSecretExpiresAt);
1202
+ * ```
1203
+ */
1204
+ async rotateSecret(id) {
1205
+ if (!id || !id.startsWith("whk_")) {
1206
+ throw new Error("Invalid webhook ID format");
1207
+ }
1208
+ const response = await this.http.request({
1209
+ method: "POST",
1210
+ path: `/webhooks/${encodeURIComponent(id)}/rotate-secret`
1211
+ });
1212
+ return transformKeys(response);
1213
+ }
1214
+ /**
1215
+ * Get delivery history for a webhook
1216
+ *
1217
+ * @param id - Webhook ID
1218
+ * @returns Array of delivery attempts
1219
+ *
1220
+ * @example
1221
+ * ```typescript
1222
+ * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx');
1223
+ *
1224
+ * for (const delivery of deliveries) {
1225
+ * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`);
1226
+ * }
1227
+ * ```
1228
+ */
1229
+ async getDeliveries(id) {
1230
+ if (!id || !id.startsWith("whk_")) {
1231
+ throw new Error("Invalid webhook ID format");
1232
+ }
1233
+ const response = await this.http.request({
1234
+ method: "GET",
1235
+ path: `/webhooks/${encodeURIComponent(id)}/deliveries`
1236
+ });
1237
+ return response.map((item) => transformKeys(item));
1238
+ }
1239
+ /**
1240
+ * Retry a failed delivery
1241
+ *
1242
+ * @param webhookId - Webhook ID
1243
+ * @param deliveryId - Delivery ID
1244
+ *
1245
+ * @example
1246
+ * ```typescript
1247
+ * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy');
1248
+ * ```
1249
+ */
1250
+ async retryDelivery(webhookId, deliveryId) {
1251
+ if (!webhookId || !webhookId.startsWith("whk_")) {
1252
+ throw new Error("Invalid webhook ID format");
1253
+ }
1254
+ if (!deliveryId || !deliveryId.startsWith("del_")) {
1255
+ throw new Error("Invalid delivery ID format");
1256
+ }
1257
+ await this.http.request({
1258
+ method: "POST",
1259
+ path: `/webhooks/${encodeURIComponent(webhookId)}/deliveries/${encodeURIComponent(deliveryId)}/retry`
1260
+ });
1261
+ }
1262
+ /**
1263
+ * List available event types
1264
+ *
1265
+ * @returns Array of event type strings
1266
+ *
1267
+ * @example
1268
+ * ```typescript
1269
+ * const eventTypes = await sendly.webhooks.listEventTypes();
1270
+ * console.log('Available events:', eventTypes);
1271
+ * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced']
1272
+ * ```
1273
+ */
1274
+ async listEventTypes() {
1275
+ const eventTypes = await this.http.request({
1276
+ method: "GET",
1277
+ path: "/webhooks/event-types"
1278
+ });
1279
+ return eventTypes;
1280
+ }
1281
+ };
1282
+
1283
+ // src/resources/account.ts
1284
+ var AccountResource = class {
1285
+ http;
1286
+ constructor(http) {
1287
+ this.http = http;
1288
+ }
1289
+ /**
1290
+ * Get account information
1291
+ *
1292
+ * @returns Account details
1293
+ *
1294
+ * @example
1295
+ * ```typescript
1296
+ * const account = await sendly.account.get();
1297
+ * console.log(`Account: ${account.email}`);
1298
+ * ```
1299
+ */
1300
+ async get() {
1301
+ const account = await this.http.request({
1302
+ method: "GET",
1303
+ path: "/account"
1304
+ });
1305
+ return account;
1306
+ }
1307
+ /**
1308
+ * Get credit balance
1309
+ *
1310
+ * @returns Current credit balance and reserved credits
1311
+ *
1312
+ * @example
1313
+ * ```typescript
1314
+ * const credits = await sendly.account.getCredits();
1315
+ *
1316
+ * console.log(`Total balance: ${credits.balance}`);
1317
+ * console.log(`Reserved (scheduled): ${credits.reservedBalance}`);
1318
+ * console.log(`Available to use: ${credits.availableBalance}`);
1319
+ * ```
1320
+ */
1321
+ async getCredits() {
1322
+ const credits = await this.http.request({
1323
+ method: "GET",
1324
+ path: "/credits"
1325
+ });
1326
+ return credits;
1327
+ }
1328
+ /**
1329
+ * Get credit transaction history
1330
+ *
1331
+ * @param options - Pagination options
1332
+ * @returns Array of credit transactions
1333
+ *
1334
+ * @example
1335
+ * ```typescript
1336
+ * const transactions = await sendly.account.getCreditTransactions();
1337
+ *
1338
+ * for (const tx of transactions) {
1339
+ * const sign = tx.amount > 0 ? '+' : '';
1340
+ * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`);
1341
+ * }
1342
+ * ```
1343
+ */
1344
+ async getCreditTransactions(options) {
1345
+ const transactions = await this.http.request({
1346
+ method: "GET",
1347
+ path: "/credits/transactions",
1348
+ query: {
1349
+ limit: options?.limit,
1350
+ offset: options?.offset
1351
+ }
1352
+ });
1353
+ return transactions;
1354
+ }
1355
+ /**
1356
+ * List API keys for the account
1357
+ *
1358
+ * Note: This returns key metadata, not the actual secret keys.
1359
+ *
1360
+ * @returns Array of API keys
1361
+ *
1362
+ * @example
1363
+ * ```typescript
1364
+ * const keys = await sendly.account.listApiKeys();
1365
+ *
1366
+ * for (const key of keys) {
1367
+ * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`);
1368
+ * }
1369
+ * ```
1370
+ */
1371
+ async listApiKeys() {
1372
+ const keys = await this.http.request({
1373
+ method: "GET",
1374
+ path: "/keys"
1375
+ });
1376
+ return keys;
1377
+ }
1378
+ /**
1379
+ * Get a specific API key by ID
1380
+ *
1381
+ * @param id - API key ID
1382
+ * @returns API key details
1383
+ *
1384
+ * @example
1385
+ * ```typescript
1386
+ * const key = await sendly.account.getApiKey('key_xxx');
1387
+ * console.log(`Last used: ${key.lastUsedAt}`);
1388
+ * ```
1389
+ */
1390
+ async getApiKey(id) {
1391
+ const key = await this.http.request({
1392
+ method: "GET",
1393
+ path: `/keys/${encodeURIComponent(id)}`
1394
+ });
1395
+ return key;
1396
+ }
1397
+ /**
1398
+ * Get usage statistics for an API key
1399
+ *
1400
+ * @param id - API key ID
1401
+ * @returns Usage statistics
1402
+ *
1403
+ * @example
1404
+ * ```typescript
1405
+ * const usage = await sendly.account.getApiKeyUsage('key_xxx');
1406
+ * console.log(`Messages sent: ${usage.messagesSent}`);
1407
+ * ```
1408
+ */
1409
+ async getApiKeyUsage(id) {
1410
+ const usage = await this.http.request({
1411
+ method: "GET",
1412
+ path: `/keys/${encodeURIComponent(id)}/usage`
1413
+ });
1414
+ return usage;
1415
+ }
1416
+ };
1417
+
981
1418
  // src/client.ts
982
1419
  var DEFAULT_BASE_URL2 = "https://sendly.live/api/v1";
983
1420
  var DEFAULT_TIMEOUT2 = 3e4;
@@ -999,6 +1436,41 @@ var Sendly = class {
999
1436
  * ```
1000
1437
  */
1001
1438
  messages;
1439
+ /**
1440
+ * Webhooks API resource
1441
+ *
1442
+ * @example
1443
+ * ```typescript
1444
+ * // Create a webhook
1445
+ * const webhook = await sendly.webhooks.create({
1446
+ * url: 'https://example.com/webhooks',
1447
+ * events: ['message.delivered', 'message.failed']
1448
+ * });
1449
+ *
1450
+ * // List webhooks
1451
+ * const webhooks = await sendly.webhooks.list();
1452
+ *
1453
+ * // Test a webhook
1454
+ * await sendly.webhooks.test('whk_xxx');
1455
+ * ```
1456
+ */
1457
+ webhooks;
1458
+ /**
1459
+ * Account API resource
1460
+ *
1461
+ * @example
1462
+ * ```typescript
1463
+ * // Get credit balance
1464
+ * const credits = await sendly.account.getCredits();
1465
+ *
1466
+ * // Get transaction history
1467
+ * const transactions = await sendly.account.getCreditTransactions();
1468
+ *
1469
+ * // List API keys
1470
+ * const keys = await sendly.account.listApiKeys();
1471
+ * ```
1472
+ */
1473
+ account;
1002
1474
  http;
1003
1475
  config;
1004
1476
  /**
@@ -1029,6 +1501,8 @@ var Sendly = class {
1029
1501
  maxRetries: this.config.maxRetries
1030
1502
  });
1031
1503
  this.messages = new MessagesResource(this.http);
1504
+ this.webhooks = new WebhooksResource(this.http);
1505
+ this.account = new AccountResource(this.http);
1032
1506
  }
1033
1507
  /**
1034
1508
  * Check if the client is using a test API key
@@ -1082,11 +1556,22 @@ var WebhookSignatureError = class extends Error {
1082
1556
  this.name = "WebhookSignatureError";
1083
1557
  }
1084
1558
  };
1085
- function verifyWebhookSignature(payload, signature, secret) {
1559
+ function verifyWebhookSignature(payload, signature, secret, timestamp, toleranceSeconds = 300) {
1086
1560
  if (!payload || !signature || !secret) {
1087
1561
  return false;
1088
1562
  }
1089
- const expectedSignature = generateWebhookSignature(payload, secret);
1563
+ if (timestamp) {
1564
+ const timestampNum = parseInt(timestamp, 10);
1565
+ if (isNaN(timestampNum)) {
1566
+ return false;
1567
+ }
1568
+ const now = Math.floor(Date.now() / 1e3);
1569
+ if (Math.abs(now - timestampNum) > toleranceSeconds) {
1570
+ return false;
1571
+ }
1572
+ }
1573
+ const signedPayload = timestamp ? `${timestamp}.${payload}` : payload;
1574
+ const expectedSignature = generateWebhookSignature(signedPayload, secret);
1090
1575
  try {
1091
1576
  return crypto.timingSafeEqual(
1092
1577
  Buffer.from(signature),
@@ -1096,8 +1581,8 @@ function verifyWebhookSignature(payload, signature, secret) {
1096
1581
  return false;
1097
1582
  }
1098
1583
  }
1099
- function parseWebhookEvent(payload, signature, secret) {
1100
- if (!verifyWebhookSignature(payload, signature, secret)) {
1584
+ function parseWebhookEvent(payload, signature, secret, timestamp) {
1585
+ if (!verifyWebhookSignature(payload, signature, secret, timestamp)) {
1101
1586
  throw new WebhookSignatureError();
1102
1587
  }
1103
1588
  let event;
@@ -1106,7 +1591,7 @@ function parseWebhookEvent(payload, signature, secret) {
1106
1591
  } catch {
1107
1592
  throw new Error("Failed to parse webhook payload");
1108
1593
  }
1109
- if (!event.id || !event.type || !event.createdAt) {
1594
+ if (!event.id || !event.type || !event.created || !event.data?.object) {
1110
1595
  throw new Error("Invalid webhook event structure");
1111
1596
  }
1112
1597
  return event;
@@ -1132,25 +1617,78 @@ var Webhooks = class {
1132
1617
  * Verify a webhook signature
1133
1618
  * @param payload - Raw request body
1134
1619
  * @param signature - X-Sendly-Signature header
1620
+ * @param timestamp - X-Sendly-Timestamp header (optional)
1135
1621
  */
1136
- verify(payload, signature) {
1137
- return verifyWebhookSignature(payload, signature, this.secret);
1622
+ verify(payload, signature, timestamp) {
1623
+ return verifyWebhookSignature(payload, signature, this.secret, timestamp);
1138
1624
  }
1139
1625
  /**
1140
1626
  * Parse and verify a webhook event
1141
1627
  * @param payload - Raw request body
1142
1628
  * @param signature - X-Sendly-Signature header
1629
+ * @param timestamp - X-Sendly-Timestamp header (optional)
1143
1630
  */
1144
- parse(payload, signature) {
1145
- return parseWebhookEvent(payload, signature, this.secret);
1631
+ parse(payload, signature, timestamp) {
1632
+ return parseWebhookEvent(payload, signature, this.secret, timestamp);
1146
1633
  }
1147
1634
  /**
1148
1635
  * Generate a signature for testing
1149
- * @param payload - Payload to sign
1636
+ * @param payload - Payload to sign (should include timestamp prefix if using timestamps)
1150
1637
  */
1151
1638
  sign(payload) {
1152
1639
  return generateWebhookSignature(payload, this.secret);
1153
1640
  }
1641
+ // ============================================================================
1642
+ // Static methods for backwards compatibility with existing code/tests
1643
+ // ============================================================================
1644
+ /**
1645
+ * Verify a webhook signature (static method for backwards compatibility)
1646
+ * @param payload - Raw request body
1647
+ * @param signature - X-Sendly-Signature header
1648
+ * @param secret - Your webhook secret
1649
+ */
1650
+ static verifySignature(payload, signature, secret) {
1651
+ return verifyWebhookSignature(payload, signature, secret);
1652
+ }
1653
+ /**
1654
+ * Parse and verify a webhook event (static method for backwards compatibility)
1655
+ * @param payload - Raw request body
1656
+ * @param signature - X-Sendly-Signature header
1657
+ * @param secret - Your webhook secret
1658
+ */
1659
+ static parseEvent(payload, signature, secret) {
1660
+ if (!verifyWebhookSignature(payload, signature, secret)) {
1661
+ throw new WebhookSignatureError("Invalid webhook signature");
1662
+ }
1663
+ let event;
1664
+ try {
1665
+ event = JSON.parse(payload);
1666
+ } catch {
1667
+ throw new WebhookSignatureError("Failed to parse webhook payload");
1668
+ }
1669
+ const parsed = event;
1670
+ if (!parsed.id || !parsed.type || !parsed.created_at) {
1671
+ throw new WebhookSignatureError("Invalid event structure");
1672
+ }
1673
+ if (parsed.data && typeof parsed.data === "object" && "message_id" in parsed.data) {
1674
+ return event;
1675
+ }
1676
+ if (parsed.data && typeof parsed.data === "object" && "object" in parsed.data) {
1677
+ return event;
1678
+ }
1679
+ if (!parsed.data) {
1680
+ throw new WebhookSignatureError("Invalid event structure");
1681
+ }
1682
+ return event;
1683
+ }
1684
+ /**
1685
+ * Generate a webhook signature (static method for backwards compatibility)
1686
+ * @param payload - Payload to sign
1687
+ * @param secret - Secret to use for signing
1688
+ */
1689
+ static generateSignature(payload, secret) {
1690
+ return generateWebhookSignature(payload, secret);
1691
+ }
1154
1692
  };
1155
1693
  // Annotate the CommonJS export names for ESM import in node:
1156
1694
  0 && (module.exports = {