@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.d.mts +757 -49
- package/dist/index.d.ts +757 -49
- package/dist/index.js +559 -21
- package/dist/index.mjs +559 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -631,7 +631,7 @@ var MessagesResource = class {
|
|
|
631
631
|
}
|
|
632
632
|
const message = await this.http.request({
|
|
633
633
|
method: "POST",
|
|
634
|
-
path: "/
|
|
634
|
+
path: "/messages",
|
|
635
635
|
body: {
|
|
636
636
|
to: request.to,
|
|
637
637
|
text: request.text,
|
|
@@ -667,7 +667,7 @@ var MessagesResource = class {
|
|
|
667
667
|
validateLimit(options.limit);
|
|
668
668
|
const response = await this.http.request({
|
|
669
669
|
method: "GET",
|
|
670
|
-
path: "/
|
|
670
|
+
path: "/messages",
|
|
671
671
|
query: {
|
|
672
672
|
limit: options.limit,
|
|
673
673
|
offset: options.offset,
|
|
@@ -698,7 +698,7 @@ var MessagesResource = class {
|
|
|
698
698
|
validateMessageId(id);
|
|
699
699
|
const message = await this.http.request({
|
|
700
700
|
method: "GET",
|
|
701
|
-
path: `/
|
|
701
|
+
path: `/messages/${encodeURIComponent(id)}`
|
|
702
702
|
});
|
|
703
703
|
return message;
|
|
704
704
|
}
|
|
@@ -731,7 +731,7 @@ var MessagesResource = class {
|
|
|
731
731
|
while (hasMore) {
|
|
732
732
|
const response = await this.http.request({
|
|
733
733
|
method: "GET",
|
|
734
|
-
path: "/
|
|
734
|
+
path: "/messages",
|
|
735
735
|
query: {
|
|
736
736
|
limit: batchSize,
|
|
737
737
|
offset
|
|
@@ -790,7 +790,7 @@ var MessagesResource = class {
|
|
|
790
790
|
}
|
|
791
791
|
const scheduled = await this.http.request({
|
|
792
792
|
method: "POST",
|
|
793
|
-
path: "/
|
|
793
|
+
path: "/messages/schedule",
|
|
794
794
|
body: {
|
|
795
795
|
to: request.to,
|
|
796
796
|
text: request.text,
|
|
@@ -819,7 +819,7 @@ var MessagesResource = class {
|
|
|
819
819
|
validateLimit(options.limit);
|
|
820
820
|
const response = await this.http.request({
|
|
821
821
|
method: "GET",
|
|
822
|
-
path: "/
|
|
822
|
+
path: "/messages/scheduled",
|
|
823
823
|
query: {
|
|
824
824
|
limit: options.limit,
|
|
825
825
|
offset: options.offset,
|
|
@@ -844,7 +844,7 @@ var MessagesResource = class {
|
|
|
844
844
|
validateMessageId(id);
|
|
845
845
|
const scheduled = await this.http.request({
|
|
846
846
|
method: "GET",
|
|
847
|
-
path: `/
|
|
847
|
+
path: `/messages/scheduled/${encodeURIComponent(id)}`
|
|
848
848
|
});
|
|
849
849
|
return scheduled;
|
|
850
850
|
}
|
|
@@ -869,7 +869,7 @@ var MessagesResource = class {
|
|
|
869
869
|
validateMessageId(id);
|
|
870
870
|
const result = await this.http.request({
|
|
871
871
|
method: "DELETE",
|
|
872
|
-
path: `/
|
|
872
|
+
path: `/messages/scheduled/${encodeURIComponent(id)}`
|
|
873
873
|
});
|
|
874
874
|
return result;
|
|
875
875
|
}
|
|
@@ -915,7 +915,7 @@ var MessagesResource = class {
|
|
|
915
915
|
}
|
|
916
916
|
const batch = await this.http.request({
|
|
917
917
|
method: "POST",
|
|
918
|
-
path: "/
|
|
918
|
+
path: "/messages/batch",
|
|
919
919
|
body: {
|
|
920
920
|
messages: request.messages,
|
|
921
921
|
...request.from && { from: request.from }
|
|
@@ -944,7 +944,7 @@ var MessagesResource = class {
|
|
|
944
944
|
}
|
|
945
945
|
const batch = await this.http.request({
|
|
946
946
|
method: "GET",
|
|
947
|
-
path: `/
|
|
947
|
+
path: `/messages/batch/${encodeURIComponent(batchId)}`
|
|
948
948
|
});
|
|
949
949
|
return batch;
|
|
950
950
|
}
|
|
@@ -967,7 +967,7 @@ var MessagesResource = class {
|
|
|
967
967
|
validateLimit(options.limit);
|
|
968
968
|
const response = await this.http.request({
|
|
969
969
|
method: "GET",
|
|
970
|
-
path: "/
|
|
970
|
+
path: "/messages/batches",
|
|
971
971
|
query: {
|
|
972
972
|
limit: options.limit,
|
|
973
973
|
offset: options.offset,
|
|
@@ -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 = {
|