@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.d.mts +757 -49
- package/dist/index.d.ts +757 -49
- package/dist/index.js +548 -10
- package/dist/index.mjs +548 -10
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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 = {
|