@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.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: "/
|
|
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: "/
|
|
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: `/
|
|
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: "/
|
|
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: "/
|
|
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: "/
|
|
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: `/
|
|
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: `/
|
|
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: "/
|
|
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: `/
|
|
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: "/
|
|
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
|
-
|
|
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.
|
|
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,
|