@sendly/node 1.0.7 → 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.mjs CHANGED
@@ -571,7 +571,7 @@ var MessagesResource = class {
571
571
  }
572
572
  const message = await this.http.request({
573
573
  method: "POST",
574
- path: "/v1/messages",
574
+ path: "/messages",
575
575
  body: {
576
576
  to: request.to,
577
577
  text: request.text,
@@ -607,7 +607,7 @@ var MessagesResource = class {
607
607
  validateLimit(options.limit);
608
608
  const response = await this.http.request({
609
609
  method: "GET",
610
- path: "/v1/messages",
610
+ path: "/messages",
611
611
  query: {
612
612
  limit: options.limit,
613
613
  offset: options.offset,
@@ -638,7 +638,7 @@ var MessagesResource = class {
638
638
  validateMessageId(id);
639
639
  const message = await this.http.request({
640
640
  method: "GET",
641
- path: `/v1/messages/${encodeURIComponent(id)}`
641
+ path: `/messages/${encodeURIComponent(id)}`
642
642
  });
643
643
  return message;
644
644
  }
@@ -671,7 +671,7 @@ var MessagesResource = class {
671
671
  while (hasMore) {
672
672
  const response = await this.http.request({
673
673
  method: "GET",
674
- path: "/v1/messages",
674
+ path: "/messages",
675
675
  query: {
676
676
  limit: batchSize,
677
677
  offset
@@ -730,7 +730,7 @@ var MessagesResource = class {
730
730
  }
731
731
  const scheduled = await this.http.request({
732
732
  method: "POST",
733
- path: "/v1/messages/schedule",
733
+ path: "/messages/schedule",
734
734
  body: {
735
735
  to: request.to,
736
736
  text: request.text,
@@ -759,7 +759,7 @@ var MessagesResource = class {
759
759
  validateLimit(options.limit);
760
760
  const response = await this.http.request({
761
761
  method: "GET",
762
- path: "/v1/messages/scheduled",
762
+ path: "/messages/scheduled",
763
763
  query: {
764
764
  limit: options.limit,
765
765
  offset: options.offset,
@@ -784,7 +784,7 @@ var MessagesResource = class {
784
784
  validateMessageId(id);
785
785
  const scheduled = await this.http.request({
786
786
  method: "GET",
787
- path: `/v1/messages/scheduled/${encodeURIComponent(id)}`
787
+ path: `/messages/scheduled/${encodeURIComponent(id)}`
788
788
  });
789
789
  return scheduled;
790
790
  }
@@ -809,7 +809,7 @@ var MessagesResource = class {
809
809
  validateMessageId(id);
810
810
  const result = await this.http.request({
811
811
  method: "DELETE",
812
- path: `/v1/messages/scheduled/${encodeURIComponent(id)}`
812
+ path: `/messages/scheduled/${encodeURIComponent(id)}`
813
813
  });
814
814
  return result;
815
815
  }
@@ -855,7 +855,7 @@ var MessagesResource = class {
855
855
  }
856
856
  const batch = await this.http.request({
857
857
  method: "POST",
858
- path: "/v1/messages/batch",
858
+ path: "/messages/batch",
859
859
  body: {
860
860
  messages: request.messages,
861
861
  ...request.from && { from: request.from }
@@ -884,7 +884,7 @@ var MessagesResource = class {
884
884
  }
885
885
  const batch = await this.http.request({
886
886
  method: "GET",
887
- path: `/v1/messages/batch/${encodeURIComponent(batchId)}`
887
+ path: `/messages/batch/${encodeURIComponent(batchId)}`
888
888
  });
889
889
  return batch;
890
890
  }
@@ -907,7 +907,7 @@ var MessagesResource = class {
907
907
  validateLimit(options.limit);
908
908
  const response = await this.http.request({
909
909
  method: "GET",
910
- path: "/v1/messages/batches",
910
+ path: "/messages/batches",
911
911
  query: {
912
912
  limit: options.limit,
913
913
  offset: options.offset,
@@ -918,6 +918,443 @@ var MessagesResource = class {
918
918
  }
919
919
  };
920
920
 
921
+ // src/utils/transform.ts
922
+ function snakeToCamel(str) {
923
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
924
+ }
925
+ function transformKeys(obj) {
926
+ if (obj === null || obj === void 0) {
927
+ return obj;
928
+ }
929
+ if (Array.isArray(obj)) {
930
+ return obj.map((item) => transformKeys(item));
931
+ }
932
+ if (typeof obj === "object") {
933
+ const result = {};
934
+ for (const [key, value] of Object.entries(obj)) {
935
+ const camelKey = snakeToCamel(key);
936
+ result[camelKey] = transformKeys(value);
937
+ }
938
+ return result;
939
+ }
940
+ return obj;
941
+ }
942
+
943
+ // src/resources/webhooks.ts
944
+ var WebhooksResource = class {
945
+ http;
946
+ constructor(http) {
947
+ this.http = http;
948
+ }
949
+ /**
950
+ * Create a new webhook endpoint
951
+ *
952
+ * @param options - Webhook configuration
953
+ * @returns The created webhook with signing secret (shown only once!)
954
+ *
955
+ * @example
956
+ * ```typescript
957
+ * const webhook = await sendly.webhooks.create({
958
+ * url: 'https://example.com/webhooks/sendly',
959
+ * events: ['message.delivered', 'message.failed'],
960
+ * description: 'Production webhook'
961
+ * });
962
+ *
963
+ * // IMPORTANT: Save this secret securely - it's only shown once!
964
+ * console.log('Webhook secret:', webhook.secret);
965
+ * ```
966
+ *
967
+ * @throws {ValidationError} If the URL is invalid or events are empty
968
+ * @throws {AuthenticationError} If the API key is invalid
969
+ */
970
+ async create(options) {
971
+ if (!options.url || !options.url.startsWith("https://")) {
972
+ throw new Error("Webhook URL must be HTTPS");
973
+ }
974
+ if (!options.events || options.events.length === 0) {
975
+ throw new Error("At least one event type is required");
976
+ }
977
+ const response = await this.http.request({
978
+ method: "POST",
979
+ path: "/webhooks",
980
+ body: {
981
+ url: options.url,
982
+ events: options.events,
983
+ ...options.description && { description: options.description },
984
+ ...options.metadata && { metadata: options.metadata }
985
+ }
986
+ });
987
+ return transformKeys(response);
988
+ }
989
+ /**
990
+ * List all webhooks
991
+ *
992
+ * @returns Array of webhook configurations
993
+ *
994
+ * @example
995
+ * ```typescript
996
+ * const webhooks = await sendly.webhooks.list();
997
+ *
998
+ * for (const webhook of webhooks) {
999
+ * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`);
1000
+ * }
1001
+ * ```
1002
+ */
1003
+ async list() {
1004
+ const response = await this.http.request({
1005
+ method: "GET",
1006
+ path: "/webhooks"
1007
+ });
1008
+ return response.map((item) => transformKeys(item));
1009
+ }
1010
+ /**
1011
+ * Get a specific webhook by ID
1012
+ *
1013
+ * @param id - Webhook ID (whk_xxx)
1014
+ * @returns The webhook details
1015
+ *
1016
+ * @example
1017
+ * ```typescript
1018
+ * const webhook = await sendly.webhooks.get('whk_xxx');
1019
+ * console.log(`Success rate: ${webhook.successRate}%`);
1020
+ * ```
1021
+ *
1022
+ * @throws {NotFoundError} If the webhook doesn't exist
1023
+ */
1024
+ async get(id) {
1025
+ if (!id || !id.startsWith("whk_")) {
1026
+ throw new Error("Invalid webhook ID format");
1027
+ }
1028
+ const response = await this.http.request({
1029
+ method: "GET",
1030
+ path: `/webhooks/${encodeURIComponent(id)}`
1031
+ });
1032
+ return transformKeys(response);
1033
+ }
1034
+ /**
1035
+ * Update a webhook configuration
1036
+ *
1037
+ * @param id - Webhook ID
1038
+ * @param options - Fields to update
1039
+ * @returns The updated webhook
1040
+ *
1041
+ * @example
1042
+ * ```typescript
1043
+ * // Update URL
1044
+ * await sendly.webhooks.update('whk_xxx', {
1045
+ * url: 'https://new-endpoint.example.com/webhooks'
1046
+ * });
1047
+ *
1048
+ * // Disable webhook
1049
+ * await sendly.webhooks.update('whk_xxx', { isActive: false });
1050
+ *
1051
+ * // Change event subscriptions
1052
+ * await sendly.webhooks.update('whk_xxx', {
1053
+ * events: ['message.delivered']
1054
+ * });
1055
+ * ```
1056
+ */
1057
+ async update(id, options) {
1058
+ if (!id || !id.startsWith("whk_")) {
1059
+ throw new Error("Invalid webhook ID format");
1060
+ }
1061
+ if (options.url && !options.url.startsWith("https://")) {
1062
+ throw new Error("Webhook URL must be HTTPS");
1063
+ }
1064
+ const response = await this.http.request({
1065
+ method: "PATCH",
1066
+ path: `/webhooks/${encodeURIComponent(id)}`,
1067
+ body: {
1068
+ ...options.url !== void 0 && { url: options.url },
1069
+ ...options.events !== void 0 && { events: options.events },
1070
+ ...options.description !== void 0 && {
1071
+ description: options.description
1072
+ },
1073
+ ...options.isActive !== void 0 && { is_active: options.isActive },
1074
+ ...options.metadata !== void 0 && { metadata: options.metadata }
1075
+ }
1076
+ });
1077
+ return transformKeys(response);
1078
+ }
1079
+ /**
1080
+ * Delete a webhook
1081
+ *
1082
+ * @param id - Webhook ID
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * await sendly.webhooks.delete('whk_xxx');
1087
+ * ```
1088
+ *
1089
+ * @throws {NotFoundError} If the webhook doesn't exist
1090
+ */
1091
+ async delete(id) {
1092
+ if (!id || !id.startsWith("whk_")) {
1093
+ throw new Error("Invalid webhook ID format");
1094
+ }
1095
+ await this.http.request({
1096
+ method: "DELETE",
1097
+ path: `/webhooks/${encodeURIComponent(id)}`
1098
+ });
1099
+ }
1100
+ /**
1101
+ * Send a test event to a webhook endpoint
1102
+ *
1103
+ * @param id - Webhook ID
1104
+ * @returns Test result with response details
1105
+ *
1106
+ * @example
1107
+ * ```typescript
1108
+ * const result = await sendly.webhooks.test('whk_xxx');
1109
+ *
1110
+ * if (result.success) {
1111
+ * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`);
1112
+ * } else {
1113
+ * console.log(`Test failed: ${result.error}`);
1114
+ * }
1115
+ * ```
1116
+ */
1117
+ async test(id) {
1118
+ if (!id || !id.startsWith("whk_")) {
1119
+ throw new Error("Invalid webhook ID format");
1120
+ }
1121
+ const response = await this.http.request({
1122
+ method: "POST",
1123
+ path: `/webhooks/${encodeURIComponent(id)}/test`
1124
+ });
1125
+ return transformKeys(response);
1126
+ }
1127
+ /**
1128
+ * Rotate the webhook signing secret
1129
+ *
1130
+ * The old secret remains valid for 24 hours to allow for graceful migration.
1131
+ *
1132
+ * @param id - Webhook ID
1133
+ * @returns New secret and expiration info
1134
+ *
1135
+ * @example
1136
+ * ```typescript
1137
+ * const rotation = await sendly.webhooks.rotateSecret('whk_xxx');
1138
+ *
1139
+ * // Update your webhook handler with the new secret
1140
+ * console.log('New secret:', rotation.newSecret);
1141
+ * console.log('Old secret expires:', rotation.oldSecretExpiresAt);
1142
+ * ```
1143
+ */
1144
+ async rotateSecret(id) {
1145
+ if (!id || !id.startsWith("whk_")) {
1146
+ throw new Error("Invalid webhook ID format");
1147
+ }
1148
+ const response = await this.http.request({
1149
+ method: "POST",
1150
+ path: `/webhooks/${encodeURIComponent(id)}/rotate-secret`
1151
+ });
1152
+ return transformKeys(response);
1153
+ }
1154
+ /**
1155
+ * Get delivery history for a webhook
1156
+ *
1157
+ * @param id - Webhook ID
1158
+ * @returns Array of delivery attempts
1159
+ *
1160
+ * @example
1161
+ * ```typescript
1162
+ * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx');
1163
+ *
1164
+ * for (const delivery of deliveries) {
1165
+ * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`);
1166
+ * }
1167
+ * ```
1168
+ */
1169
+ async getDeliveries(id) {
1170
+ if (!id || !id.startsWith("whk_")) {
1171
+ throw new Error("Invalid webhook ID format");
1172
+ }
1173
+ const response = await this.http.request({
1174
+ method: "GET",
1175
+ path: `/webhooks/${encodeURIComponent(id)}/deliveries`
1176
+ });
1177
+ return response.map((item) => transformKeys(item));
1178
+ }
1179
+ /**
1180
+ * Retry a failed delivery
1181
+ *
1182
+ * @param webhookId - Webhook ID
1183
+ * @param deliveryId - Delivery ID
1184
+ *
1185
+ * @example
1186
+ * ```typescript
1187
+ * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy');
1188
+ * ```
1189
+ */
1190
+ async retryDelivery(webhookId, deliveryId) {
1191
+ if (!webhookId || !webhookId.startsWith("whk_")) {
1192
+ throw new Error("Invalid webhook ID format");
1193
+ }
1194
+ if (!deliveryId || !deliveryId.startsWith("del_")) {
1195
+ throw new Error("Invalid delivery ID format");
1196
+ }
1197
+ await this.http.request({
1198
+ method: "POST",
1199
+ path: `/webhooks/${encodeURIComponent(webhookId)}/deliveries/${encodeURIComponent(deliveryId)}/retry`
1200
+ });
1201
+ }
1202
+ /**
1203
+ * List available event types
1204
+ *
1205
+ * @returns Array of event type strings
1206
+ *
1207
+ * @example
1208
+ * ```typescript
1209
+ * const eventTypes = await sendly.webhooks.listEventTypes();
1210
+ * console.log('Available events:', eventTypes);
1211
+ * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced']
1212
+ * ```
1213
+ */
1214
+ async listEventTypes() {
1215
+ const eventTypes = await this.http.request({
1216
+ method: "GET",
1217
+ path: "/webhooks/event-types"
1218
+ });
1219
+ return eventTypes;
1220
+ }
1221
+ };
1222
+
1223
+ // src/resources/account.ts
1224
+ var AccountResource = class {
1225
+ http;
1226
+ constructor(http) {
1227
+ this.http = http;
1228
+ }
1229
+ /**
1230
+ * Get account information
1231
+ *
1232
+ * @returns Account details
1233
+ *
1234
+ * @example
1235
+ * ```typescript
1236
+ * const account = await sendly.account.get();
1237
+ * console.log(`Account: ${account.email}`);
1238
+ * ```
1239
+ */
1240
+ async get() {
1241
+ const account = await this.http.request({
1242
+ method: "GET",
1243
+ path: "/account"
1244
+ });
1245
+ return account;
1246
+ }
1247
+ /**
1248
+ * Get credit balance
1249
+ *
1250
+ * @returns Current credit balance and reserved credits
1251
+ *
1252
+ * @example
1253
+ * ```typescript
1254
+ * const credits = await sendly.account.getCredits();
1255
+ *
1256
+ * console.log(`Total balance: ${credits.balance}`);
1257
+ * console.log(`Reserved (scheduled): ${credits.reservedBalance}`);
1258
+ * console.log(`Available to use: ${credits.availableBalance}`);
1259
+ * ```
1260
+ */
1261
+ async getCredits() {
1262
+ const credits = await this.http.request({
1263
+ method: "GET",
1264
+ path: "/credits"
1265
+ });
1266
+ return credits;
1267
+ }
1268
+ /**
1269
+ * Get credit transaction history
1270
+ *
1271
+ * @param options - Pagination options
1272
+ * @returns Array of credit transactions
1273
+ *
1274
+ * @example
1275
+ * ```typescript
1276
+ * const transactions = await sendly.account.getCreditTransactions();
1277
+ *
1278
+ * for (const tx of transactions) {
1279
+ * const sign = tx.amount > 0 ? '+' : '';
1280
+ * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`);
1281
+ * }
1282
+ * ```
1283
+ */
1284
+ async getCreditTransactions(options) {
1285
+ const transactions = await this.http.request({
1286
+ method: "GET",
1287
+ path: "/credits/transactions",
1288
+ query: {
1289
+ limit: options?.limit,
1290
+ offset: options?.offset
1291
+ }
1292
+ });
1293
+ return transactions;
1294
+ }
1295
+ /**
1296
+ * List API keys for the account
1297
+ *
1298
+ * Note: This returns key metadata, not the actual secret keys.
1299
+ *
1300
+ * @returns Array of API keys
1301
+ *
1302
+ * @example
1303
+ * ```typescript
1304
+ * const keys = await sendly.account.listApiKeys();
1305
+ *
1306
+ * for (const key of keys) {
1307
+ * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`);
1308
+ * }
1309
+ * ```
1310
+ */
1311
+ async listApiKeys() {
1312
+ const keys = await this.http.request({
1313
+ method: "GET",
1314
+ path: "/keys"
1315
+ });
1316
+ return keys;
1317
+ }
1318
+ /**
1319
+ * Get a specific API key by ID
1320
+ *
1321
+ * @param id - API key ID
1322
+ * @returns API key details
1323
+ *
1324
+ * @example
1325
+ * ```typescript
1326
+ * const key = await sendly.account.getApiKey('key_xxx');
1327
+ * console.log(`Last used: ${key.lastUsedAt}`);
1328
+ * ```
1329
+ */
1330
+ async getApiKey(id) {
1331
+ const key = await this.http.request({
1332
+ method: "GET",
1333
+ path: `/keys/${encodeURIComponent(id)}`
1334
+ });
1335
+ return key;
1336
+ }
1337
+ /**
1338
+ * Get usage statistics for an API key
1339
+ *
1340
+ * @param id - API key ID
1341
+ * @returns Usage statistics
1342
+ *
1343
+ * @example
1344
+ * ```typescript
1345
+ * const usage = await sendly.account.getApiKeyUsage('key_xxx');
1346
+ * console.log(`Messages sent: ${usage.messagesSent}`);
1347
+ * ```
1348
+ */
1349
+ async getApiKeyUsage(id) {
1350
+ const usage = await this.http.request({
1351
+ method: "GET",
1352
+ path: `/keys/${encodeURIComponent(id)}/usage`
1353
+ });
1354
+ return usage;
1355
+ }
1356
+ };
1357
+
921
1358
  // src/client.ts
922
1359
  var DEFAULT_BASE_URL2 = "https://sendly.live/api/v1";
923
1360
  var DEFAULT_TIMEOUT2 = 3e4;
@@ -939,6 +1376,41 @@ var Sendly = class {
939
1376
  * ```
940
1377
  */
941
1378
  messages;
1379
+ /**
1380
+ * Webhooks API resource
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * // Create a webhook
1385
+ * const webhook = await sendly.webhooks.create({
1386
+ * url: 'https://example.com/webhooks',
1387
+ * events: ['message.delivered', 'message.failed']
1388
+ * });
1389
+ *
1390
+ * // List webhooks
1391
+ * const webhooks = await sendly.webhooks.list();
1392
+ *
1393
+ * // Test a webhook
1394
+ * await sendly.webhooks.test('whk_xxx');
1395
+ * ```
1396
+ */
1397
+ webhooks;
1398
+ /**
1399
+ * Account API resource
1400
+ *
1401
+ * @example
1402
+ * ```typescript
1403
+ * // Get credit balance
1404
+ * const credits = await sendly.account.getCredits();
1405
+ *
1406
+ * // Get transaction history
1407
+ * const transactions = await sendly.account.getCreditTransactions();
1408
+ *
1409
+ * // List API keys
1410
+ * const keys = await sendly.account.listApiKeys();
1411
+ * ```
1412
+ */
1413
+ account;
942
1414
  http;
943
1415
  config;
944
1416
  /**
@@ -969,6 +1441,8 @@ var Sendly = class {
969
1441
  maxRetries: this.config.maxRetries
970
1442
  });
971
1443
  this.messages = new MessagesResource(this.http);
1444
+ this.webhooks = new WebhooksResource(this.http);
1445
+ this.account = new AccountResource(this.http);
972
1446
  }
973
1447
  /**
974
1448
  * Check if the client is using a test API key
@@ -1022,11 +1496,22 @@ var WebhookSignatureError = class extends Error {
1022
1496
  this.name = "WebhookSignatureError";
1023
1497
  }
1024
1498
  };
1025
- function verifyWebhookSignature(payload, signature, secret) {
1499
+ function verifyWebhookSignature(payload, signature, secret, timestamp, toleranceSeconds = 300) {
1026
1500
  if (!payload || !signature || !secret) {
1027
1501
  return false;
1028
1502
  }
1029
- const expectedSignature = generateWebhookSignature(payload, secret);
1503
+ if (timestamp) {
1504
+ const timestampNum = parseInt(timestamp, 10);
1505
+ if (isNaN(timestampNum)) {
1506
+ return false;
1507
+ }
1508
+ const now = Math.floor(Date.now() / 1e3);
1509
+ if (Math.abs(now - timestampNum) > toleranceSeconds) {
1510
+ return false;
1511
+ }
1512
+ }
1513
+ const signedPayload = timestamp ? `${timestamp}.${payload}` : payload;
1514
+ const expectedSignature = generateWebhookSignature(signedPayload, secret);
1030
1515
  try {
1031
1516
  return crypto.timingSafeEqual(
1032
1517
  Buffer.from(signature),
@@ -1036,8 +1521,8 @@ function verifyWebhookSignature(payload, signature, secret) {
1036
1521
  return false;
1037
1522
  }
1038
1523
  }
1039
- function parseWebhookEvent(payload, signature, secret) {
1040
- if (!verifyWebhookSignature(payload, signature, secret)) {
1524
+ function parseWebhookEvent(payload, signature, secret, timestamp) {
1525
+ if (!verifyWebhookSignature(payload, signature, secret, timestamp)) {
1041
1526
  throw new WebhookSignatureError();
1042
1527
  }
1043
1528
  let event;
@@ -1046,7 +1531,7 @@ function parseWebhookEvent(payload, signature, secret) {
1046
1531
  } catch {
1047
1532
  throw new Error("Failed to parse webhook payload");
1048
1533
  }
1049
- if (!event.id || !event.type || !event.createdAt) {
1534
+ if (!event.id || !event.type || !event.created || !event.data?.object) {
1050
1535
  throw new Error("Invalid webhook event structure");
1051
1536
  }
1052
1537
  return event;
@@ -1072,25 +1557,78 @@ var Webhooks = class {
1072
1557
  * Verify a webhook signature
1073
1558
  * @param payload - Raw request body
1074
1559
  * @param signature - X-Sendly-Signature header
1560
+ * @param timestamp - X-Sendly-Timestamp header (optional)
1075
1561
  */
1076
- verify(payload, signature) {
1077
- return verifyWebhookSignature(payload, signature, this.secret);
1562
+ verify(payload, signature, timestamp) {
1563
+ return verifyWebhookSignature(payload, signature, this.secret, timestamp);
1078
1564
  }
1079
1565
  /**
1080
1566
  * Parse and verify a webhook event
1081
1567
  * @param payload - Raw request body
1082
1568
  * @param signature - X-Sendly-Signature header
1569
+ * @param timestamp - X-Sendly-Timestamp header (optional)
1083
1570
  */
1084
- parse(payload, signature) {
1085
- return parseWebhookEvent(payload, signature, this.secret);
1571
+ parse(payload, signature, timestamp) {
1572
+ return parseWebhookEvent(payload, signature, this.secret, timestamp);
1086
1573
  }
1087
1574
  /**
1088
1575
  * Generate a signature for testing
1089
- * @param payload - Payload to sign
1576
+ * @param payload - Payload to sign (should include timestamp prefix if using timestamps)
1090
1577
  */
1091
1578
  sign(payload) {
1092
1579
  return generateWebhookSignature(payload, this.secret);
1093
1580
  }
1581
+ // ============================================================================
1582
+ // Static methods for backwards compatibility with existing code/tests
1583
+ // ============================================================================
1584
+ /**
1585
+ * Verify a webhook signature (static method for backwards compatibility)
1586
+ * @param payload - Raw request body
1587
+ * @param signature - X-Sendly-Signature header
1588
+ * @param secret - Your webhook secret
1589
+ */
1590
+ static verifySignature(payload, signature, secret) {
1591
+ return verifyWebhookSignature(payload, signature, secret);
1592
+ }
1593
+ /**
1594
+ * Parse and verify a webhook event (static method for backwards compatibility)
1595
+ * @param payload - Raw request body
1596
+ * @param signature - X-Sendly-Signature header
1597
+ * @param secret - Your webhook secret
1598
+ */
1599
+ static parseEvent(payload, signature, secret) {
1600
+ if (!verifyWebhookSignature(payload, signature, secret)) {
1601
+ throw new WebhookSignatureError("Invalid webhook signature");
1602
+ }
1603
+ let event;
1604
+ try {
1605
+ event = JSON.parse(payload);
1606
+ } catch {
1607
+ throw new WebhookSignatureError("Failed to parse webhook payload");
1608
+ }
1609
+ const parsed = event;
1610
+ if (!parsed.id || !parsed.type || !parsed.created_at) {
1611
+ throw new WebhookSignatureError("Invalid event structure");
1612
+ }
1613
+ if (parsed.data && typeof parsed.data === "object" && "message_id" in parsed.data) {
1614
+ return event;
1615
+ }
1616
+ if (parsed.data && typeof parsed.data === "object" && "object" in parsed.data) {
1617
+ return event;
1618
+ }
1619
+ if (!parsed.data) {
1620
+ throw new WebhookSignatureError("Invalid event structure");
1621
+ }
1622
+ return event;
1623
+ }
1624
+ /**
1625
+ * Generate a webhook signature (static method for backwards compatibility)
1626
+ * @param payload - Payload to sign
1627
+ * @param secret - Secret to use for signing
1628
+ */
1629
+ static generateSignature(payload, secret) {
1630
+ return generateWebhookSignature(payload, secret);
1631
+ }
1094
1632
  };
1095
1633
  export {
1096
1634
  ALL_SUPPORTED_COUNTRIES,