@skrybe/sdk 1.0.0 → 2.0.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/LICENSE +21 -0
- package/README.md +184 -103
- package/dist/index.cjs +1235 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +764 -0
- package/dist/index.d.ts +737 -25
- package/dist/index.js +1194 -88
- package/dist/index.js.map +1 -0
- package/package.json +48 -22
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
ACTIVITY_TYPES: () => ACTIVITY_TYPES,
|
|
26
|
+
ApiError: () => ApiError,
|
|
27
|
+
AuthenticationError: () => AuthenticationError,
|
|
28
|
+
CAMPAIGN_STATUSES: () => CAMPAIGN_STATUSES,
|
|
29
|
+
ConnectionError: () => ConnectionError,
|
|
30
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
31
|
+
InvalidRequestError: () => InvalidRequestError,
|
|
32
|
+
MAX_ACTIVITY_PAGE: () => MAX_ACTIVITY_PAGE,
|
|
33
|
+
MAX_CAMPAIGNS_PAGE: () => MAX_CAMPAIGNS_PAGE,
|
|
34
|
+
MAX_RECIPIENTS: () => MAX_RECIPIENTS,
|
|
35
|
+
MAX_SUBSCRIBERS_PAGE: () => MAX_SUBSCRIBERS_PAGE,
|
|
36
|
+
MAX_TEST_EMAILS: () => MAX_TEST_EMAILS,
|
|
37
|
+
RateLimitError: () => RateLimitError,
|
|
38
|
+
SUBSCRIBER_STATUSES: () => SUBSCRIBER_STATUSES,
|
|
39
|
+
Skrybe: () => Skrybe,
|
|
40
|
+
SkrybeError: () => SkrybeError,
|
|
41
|
+
VERSION: () => VERSION
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/errors.ts
|
|
46
|
+
var SkrybeError = class extends Error {
|
|
47
|
+
constructor(type, code, message, opts = {}) {
|
|
48
|
+
super(message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
49
|
+
/** Stable machine-readable slug, e.g. `list_not_found`. */
|
|
50
|
+
__publicField(this, "code");
|
|
51
|
+
__publicField(this, "type");
|
|
52
|
+
__publicField(this, "hint");
|
|
53
|
+
__publicField(this, "status");
|
|
54
|
+
__publicField(this, "raw");
|
|
55
|
+
this.name = "SkrybeError";
|
|
56
|
+
this.type = type;
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.hint = opts.hint;
|
|
59
|
+
this.status = opts.status;
|
|
60
|
+
this.raw = opts.raw;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var AuthenticationError = class extends SkrybeError {
|
|
64
|
+
constructor(code, message, opts) {
|
|
65
|
+
super("authentication", code, message, opts);
|
|
66
|
+
this.name = "AuthenticationError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var InvalidRequestError = class extends SkrybeError {
|
|
70
|
+
constructor(code, message, opts) {
|
|
71
|
+
super("invalid_request", code, message, opts);
|
|
72
|
+
this.name = "InvalidRequestError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var RateLimitError = class extends SkrybeError {
|
|
76
|
+
constructor(code, message, opts) {
|
|
77
|
+
super("rate_limit", code, message, opts);
|
|
78
|
+
this.name = "RateLimitError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var ConnectionError = class extends SkrybeError {
|
|
82
|
+
constructor(code, message, opts) {
|
|
83
|
+
super("connection", code, message, opts);
|
|
84
|
+
this.name = "ConnectionError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ApiError = class extends SkrybeError {
|
|
88
|
+
constructor(code, message, opts) {
|
|
89
|
+
super("api", code, message, opts);
|
|
90
|
+
this.name = "ApiError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function createError(type, code, message, opts) {
|
|
94
|
+
switch (type) {
|
|
95
|
+
case "authentication":
|
|
96
|
+
return new AuthenticationError(code, message, opts);
|
|
97
|
+
case "invalid_request":
|
|
98
|
+
return new InvalidRequestError(code, message, opts);
|
|
99
|
+
case "rate_limit":
|
|
100
|
+
return new RateLimitError(code, message, opts);
|
|
101
|
+
case "connection":
|
|
102
|
+
return new ConnectionError(code, message, opts);
|
|
103
|
+
default:
|
|
104
|
+
return new ApiError(code, message, opts);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function excerpt(body, limit = 200) {
|
|
108
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
109
|
+
if (flat === "") return "(an empty response)";
|
|
110
|
+
return flat.length > limit ? `${flat.slice(0, limit)}\u2026` : flat;
|
|
111
|
+
}
|
|
112
|
+
var LIST_IDS_HINT = "Call lists.list() to see the IDs available to this API key.";
|
|
113
|
+
var UNDER_REVIEW_HINT = "Sending is paused while the brand is reviewed for a high bounce rate. Contact Skrybe support.";
|
|
114
|
+
var DOMAIN_HINT = "Verify the From domain for this brand in the Skrybe dashboard.";
|
|
115
|
+
var PROSE_ERRORS = {
|
|
116
|
+
// Authentication
|
|
117
|
+
"api key not passed": { code: "api_key_missing", type: "authentication" },
|
|
118
|
+
"invalid api key": {
|
|
119
|
+
code: "invalid_api_key",
|
|
120
|
+
type: "authentication",
|
|
121
|
+
hint: "Generate a key in the Skrybe dashboard under Settings. Keys are cleared for every login of a brand placed under review."
|
|
122
|
+
},
|
|
123
|
+
// Request shape
|
|
124
|
+
"no data passed": { code: "no_data_passed", type: "invalid_request" },
|
|
125
|
+
"some fields are missing.": { code: "missing_fields", type: "invalid_request" },
|
|
126
|
+
// Brands and lists
|
|
127
|
+
"brand id not passed": { code: "brand_id_missing", type: "invalid_request" },
|
|
128
|
+
// Also the answer to the install's main account key, which no brand is tied to.
|
|
129
|
+
"brand does not exist": {
|
|
130
|
+
code: "brand_not_found",
|
|
131
|
+
type: "api",
|
|
132
|
+
hint: "Use an API key from a brand login; the main account key is not tied to a brand."
|
|
133
|
+
},
|
|
134
|
+
"list does not exist": { code: "list_not_found", type: "api", hint: LIST_IDS_HINT },
|
|
135
|
+
"invalid list id.": { code: "list_not_found", type: "api", hint: LIST_IDS_HINT },
|
|
136
|
+
// Also what the server produces from an id it cannot decrypt, so this is
|
|
137
|
+
// the answer to a malformed id as much as to a missing one.
|
|
138
|
+
"list id not passed": {
|
|
139
|
+
code: "list_id_invalid",
|
|
140
|
+
type: "invalid_request",
|
|
141
|
+
hint: "A list ID is the encrypted value lists.list() returns, not the number in the dashboard URL."
|
|
142
|
+
},
|
|
143
|
+
// Autoresponders
|
|
144
|
+
"autoresponder id not passed": { code: "autoresponder_id_missing", type: "invalid_request" },
|
|
145
|
+
"autoresponder does not exist": { code: "autoresponder_not_found", type: "api" },
|
|
146
|
+
"email id not passed": { code: "email_id_missing", type: "invalid_request" },
|
|
147
|
+
"autoresponder email does not exist": { code: "autoresponder_email_not_found", type: "api" },
|
|
148
|
+
"autoresponder name not passed": { code: "autoresponder_name_missing", type: "invalid_request" },
|
|
149
|
+
"type must be drip, annually or on_date": { code: "invalid_autoresponder_type", type: "invalid_request" },
|
|
150
|
+
"list already has a drip autoresponder": {
|
|
151
|
+
code: "drip_exists",
|
|
152
|
+
type: "api",
|
|
153
|
+
hint: "A list can have one drip autoresponder. Add emails to the existing one."
|
|
154
|
+
},
|
|
155
|
+
"custom_field not passed": {
|
|
156
|
+
code: "custom_field_missing",
|
|
157
|
+
type: "invalid_request",
|
|
158
|
+
hint: "annually and on_date autoresponders send on a Date custom field of the list."
|
|
159
|
+
},
|
|
160
|
+
"field is not a date field": { code: "field_not_date", type: "invalid_request" },
|
|
161
|
+
"field already has an autoresponder": { code: "field_has_autoresponder", type: "api" },
|
|
162
|
+
"subject not passed": { code: "subject_missing", type: "invalid_request" },
|
|
163
|
+
"a drip email cannot go before signup": { code: "invalid_when", type: "invalid_request" },
|
|
164
|
+
"when must be immediately, or a delay such as +3 days": { code: "invalid_when", type: "invalid_request" },
|
|
165
|
+
"when must be on, or an offset such as +9 hours or -1 days": { code: "invalid_when", type: "invalid_request" },
|
|
166
|
+
// Templates
|
|
167
|
+
"template id not passed": { code: "template_id_missing", type: "invalid_request" },
|
|
168
|
+
"template does not exist": { code: "template_not_found", type: "api" },
|
|
169
|
+
"template name not passed": { code: "template_name_missing", type: "invalid_request" },
|
|
170
|
+
"template name cannot be empty": { code: "template_name_missing", type: "invalid_request" },
|
|
171
|
+
"html not passed": { code: "html_missing", type: "invalid_request" },
|
|
172
|
+
"editor must be html or dragdrop": { code: "invalid_editor", type: "invalid_request" },
|
|
173
|
+
// List management
|
|
174
|
+
"list name not passed": { code: "list_name_missing", type: "invalid_request" },
|
|
175
|
+
"list name is too long": {
|
|
176
|
+
code: "list_name_too_long",
|
|
177
|
+
type: "invalid_request",
|
|
178
|
+
hint: "A list name can be at most 100 characters."
|
|
179
|
+
},
|
|
180
|
+
"opt_in must be single or double": { code: "invalid_opt_in", type: "invalid_request" },
|
|
181
|
+
"nothing to update": { code: "nothing_to_update", type: "invalid_request", hint: "Pass at least one field to change." },
|
|
182
|
+
"list is used by a scheduled or sending campaign": {
|
|
183
|
+
code: "list_in_use",
|
|
184
|
+
type: "api",
|
|
185
|
+
hint: "Unschedule the campaign, or wait for it to finish sending, then delete the list."
|
|
186
|
+
},
|
|
187
|
+
// Custom fields
|
|
188
|
+
"field name not passed": { code: "field_name_missing", type: "invalid_request" },
|
|
189
|
+
"invalid field name": {
|
|
190
|
+
code: "invalid_field_name",
|
|
191
|
+
type: "invalid_request",
|
|
192
|
+
hint: 'Up to 100 characters, without : % [ ] , " < or >.'
|
|
193
|
+
},
|
|
194
|
+
"name and email are built-in fields": { code: "reserved_field_name", type: "invalid_request" },
|
|
195
|
+
"type must be text or date": { code: "invalid_field_type", type: "invalid_request" },
|
|
196
|
+
"field already exists": { code: "field_exists", type: "api", hint: "Field names are case-insensitive." },
|
|
197
|
+
"field does not exist": { code: "field_not_found", type: "api" },
|
|
198
|
+
"field is used by an autoresponder": {
|
|
199
|
+
code: "field_in_use",
|
|
200
|
+
type: "api",
|
|
201
|
+
hint: "Change the autoresponder in the Skrybe dashboard first."
|
|
202
|
+
},
|
|
203
|
+
"field is used by a segment": {
|
|
204
|
+
code: "field_in_use",
|
|
205
|
+
type: "api",
|
|
206
|
+
hint: "Remove it from the segment in the Skrybe dashboard first."
|
|
207
|
+
},
|
|
208
|
+
"list was changed by another request. try again.": { code: "conflict", type: "api" },
|
|
209
|
+
// Campaigns
|
|
210
|
+
"campaign id not passed": { code: "campaign_id_missing", type: "invalid_request" },
|
|
211
|
+
"campaign does not exist": { code: "campaign_not_found", type: "api" },
|
|
212
|
+
"invalid status": { code: "invalid_status", type: "invalid_request" },
|
|
213
|
+
"invalid page": { code: "invalid_page", type: "invalid_request" },
|
|
214
|
+
"invalid limit": { code: "invalid_limit", type: "invalid_request" },
|
|
215
|
+
"invalid type": { code: "invalid_type", type: "invalid_request" },
|
|
216
|
+
// Sending a campaign
|
|
217
|
+
"list or segment id(s) not passed": {
|
|
218
|
+
code: "recipients_missing",
|
|
219
|
+
type: "invalid_request",
|
|
220
|
+
hint: "Pass at least one list or segment ID."
|
|
221
|
+
},
|
|
222
|
+
"one or more list ids are invalid": { code: "list_not_found", type: "api", hint: LIST_IDS_HINT },
|
|
223
|
+
"one or more segment ids are invalid": { code: "segment_not_found", type: "api" },
|
|
224
|
+
"campaign has already been sent": {
|
|
225
|
+
code: "campaign_not_draft",
|
|
226
|
+
type: "api",
|
|
227
|
+
hint: "It has already started sending, so it can no longer be sent, scheduled or edited."
|
|
228
|
+
},
|
|
229
|
+
"campaign is scheduled": {
|
|
230
|
+
code: "campaign_scheduled",
|
|
231
|
+
type: "api",
|
|
232
|
+
hint: "Unschedule it first, or leave it to send at its scheduled time."
|
|
233
|
+
},
|
|
234
|
+
"campaign is not scheduled": { code: "campaign_not_scheduled", type: "api" },
|
|
235
|
+
// Editing and deleting campaigns
|
|
236
|
+
"campaign is sending": {
|
|
237
|
+
code: "campaign_sending",
|
|
238
|
+
type: "api",
|
|
239
|
+
hint: "Stop it in the Skrybe dashboard first, or wait for it to finish."
|
|
240
|
+
},
|
|
241
|
+
"subject cannot be empty": { code: "subject_empty", type: "invalid_request" },
|
|
242
|
+
"html cannot be empty": { code: "html_empty", type: "invalid_request" },
|
|
243
|
+
"invalid from_email": { code: "invalid_from_email", type: "invalid_request" },
|
|
244
|
+
"invalid reply_to": { code: "invalid_reply_to", type: "invalid_request" },
|
|
245
|
+
"track_opens and track_clicks must be 0, 1 or 2": {
|
|
246
|
+
code: "invalid_tracking",
|
|
247
|
+
type: "invalid_request",
|
|
248
|
+
hint: "0 off, 1 on, 2 anonymous."
|
|
249
|
+
},
|
|
250
|
+
"campaign was changed by another request. try again.": { code: "conflict", type: "api" },
|
|
251
|
+
// Scheduling
|
|
252
|
+
"schedule_date_time not passed": { code: "schedule_missing", type: "invalid_request" },
|
|
253
|
+
"schedule_date_time is invalid": {
|
|
254
|
+
code: "invalid_schedule",
|
|
255
|
+
type: "invalid_request",
|
|
256
|
+
hint: 'Use a date and time such as "2027-06-15 18:05" or "June 15, 2027 6:05pm".'
|
|
257
|
+
},
|
|
258
|
+
"schedule_date_time is in the past": {
|
|
259
|
+
code: "schedule_in_past",
|
|
260
|
+
type: "invalid_request",
|
|
261
|
+
hint: "The time is read in the timezone given, or the account's timezone if none is."
|
|
262
|
+
},
|
|
263
|
+
"schedule_timezone is invalid": {
|
|
264
|
+
code: "invalid_timezone",
|
|
265
|
+
type: "invalid_request",
|
|
266
|
+
hint: "Use an IANA name such as Africa/Lagos or America/New_York."
|
|
267
|
+
},
|
|
268
|
+
// Test sends
|
|
269
|
+
"email addresses not passed": { code: "emails_missing", type: "invalid_request" },
|
|
270
|
+
"too many email addresses": {
|
|
271
|
+
code: "too_many_emails",
|
|
272
|
+
type: "invalid_request",
|
|
273
|
+
hint: "A test send goes to at most 5 addresses."
|
|
274
|
+
},
|
|
275
|
+
"quota exceeded. please upgrade your plan.": { code: "quota_exceeded", type: "rate_limit" },
|
|
276
|
+
"no active subscribers to send to": {
|
|
277
|
+
code: "no_recipients",
|
|
278
|
+
type: "api",
|
|
279
|
+
hint: "Every subscriber on those lists is unsubscribed, bounced, unconfirmed or excluded."
|
|
280
|
+
},
|
|
281
|
+
// Subscribers
|
|
282
|
+
"subscriber does not exist": { code: "subscriber_not_found", type: "api" },
|
|
283
|
+
"email does not exist in list": { code: "subscriber_not_found", type: "api" },
|
|
284
|
+
"email not passed": { code: "email_missing", type: "invalid_request" },
|
|
285
|
+
"email address not passed": { code: "email_missing", type: "invalid_request" },
|
|
286
|
+
"invalid email address.": { code: "invalid_email", type: "invalid_request" },
|
|
287
|
+
"bounced email address.": { code: "bounced_email", type: "api" },
|
|
288
|
+
"email is suppressed.": { code: "suppressed_email", type: "api" },
|
|
289
|
+
// unsubscribe.php's wording for an address that is not on the list.
|
|
290
|
+
"email does not exist.": { code: "subscriber_not_found", type: "api" },
|
|
291
|
+
"ip address is invalid.": { code: "invalid_ip_address", type: "invalid_request" },
|
|
292
|
+
"country must be a valid 2 letter country code": {
|
|
293
|
+
code: "invalid_country",
|
|
294
|
+
type: "invalid_request",
|
|
295
|
+
hint: "Use a two-letter ISO 3166-1 code, e.g. NG."
|
|
296
|
+
},
|
|
297
|
+
"referrer is not a valid url": { code: "invalid_referrer", type: "invalid_request" },
|
|
298
|
+
"consent not given.": {
|
|
299
|
+
code: "consent_not_given",
|
|
300
|
+
type: "invalid_request",
|
|
301
|
+
hint: "The list requires GDPR consent; pass gdpr: true."
|
|
302
|
+
},
|
|
303
|
+
"failed recaptcha test.": {
|
|
304
|
+
code: "recaptcha_failed",
|
|
305
|
+
type: "api",
|
|
306
|
+
hint: "The list has reCAPTCHA enabled on its subscribe form, which an API call cannot satisfy."
|
|
307
|
+
},
|
|
308
|
+
// Sending
|
|
309
|
+
"unauthorised from email domain": {
|
|
310
|
+
code: "unauthorised_from_domain",
|
|
311
|
+
type: "api",
|
|
312
|
+
hint: "The From domain must be verified for this brand, or listed in its allowed domains."
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var PROSE_PATTERN_ERRORS = [
|
|
316
|
+
[/^error: this campaign would exceed/, { code: "quota_exceeded", type: "rate_limit" }],
|
|
317
|
+
[/^account under review:/, { code: "account_under_review", type: "api", hint: UNDER_REVIEW_HINT }],
|
|
318
|
+
[/^brand is under review/, { code: "account_under_review", type: "api", hint: UNDER_REVIEW_HINT }],
|
|
319
|
+
[/^a field is too long/, { code: "field_too_long", type: "invalid_request" }],
|
|
320
|
+
[
|
|
321
|
+
/^test send rate limit reached/,
|
|
322
|
+
{
|
|
323
|
+
code: "rate_limited",
|
|
324
|
+
type: "rate_limit",
|
|
325
|
+
hint: "Test sends are limited to 20 an hour per brand, shared with the Skrybe dashboard."
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
[/^domain not verified in ses/, { code: "domain_not_verified", type: "api", hint: DOMAIN_HINT }],
|
|
329
|
+
[/^domain ".*" is not verified/, { code: "domain_not_verified", type: "api", hint: DOMAIN_HINT }]
|
|
330
|
+
];
|
|
331
|
+
var PROSE_EMPTY = /* @__PURE__ */ new Set(["no lists found", "no brands found", "no campaigns found"]);
|
|
332
|
+
function isProseEmpty(body) {
|
|
333
|
+
return PROSE_EMPTY.has(body.trim().toLowerCase());
|
|
334
|
+
}
|
|
335
|
+
var PROSE_OK = /* @__PURE__ */ new Set(["1", "true", "already subscribed."]);
|
|
336
|
+
function isProseSuccess(body) {
|
|
337
|
+
return PROSE_OK.has(body.trim().toLowerCase());
|
|
338
|
+
}
|
|
339
|
+
function classifyProse(body, status) {
|
|
340
|
+
const trimmed = body.trim();
|
|
341
|
+
const key = trimmed.toLowerCase();
|
|
342
|
+
const rule = PROSE_ERRORS[key] ?? PROSE_PATTERN_ERRORS.find(([pattern]) => pattern.test(key))?.[1];
|
|
343
|
+
if (rule) return createError(rule.type, rule.code, trimmed, { raw: body, status, hint: rule.hint });
|
|
344
|
+
if (key.startsWith("unable to")) return new ApiError("operation_failed", trimmed, { raw: body, status });
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
function typeForStatus(status) {
|
|
348
|
+
if (status === 401 || status === 403) return "authentication";
|
|
349
|
+
if (status === 402 || status === 429) return "rate_limit";
|
|
350
|
+
if (status === 400 || status === 405 || status === 422) return "invalid_request";
|
|
351
|
+
return "api";
|
|
352
|
+
}
|
|
353
|
+
function codeForStatus(status) {
|
|
354
|
+
switch (status) {
|
|
355
|
+
case 400:
|
|
356
|
+
return "invalid_request";
|
|
357
|
+
case 401:
|
|
358
|
+
return "unauthenticated";
|
|
359
|
+
case 402:
|
|
360
|
+
return "quota_exceeded";
|
|
361
|
+
case 403:
|
|
362
|
+
return "forbidden";
|
|
363
|
+
case 404:
|
|
364
|
+
return "not_found";
|
|
365
|
+
case 405:
|
|
366
|
+
return "method_not_allowed";
|
|
367
|
+
case 429:
|
|
368
|
+
return "rate_limited";
|
|
369
|
+
default:
|
|
370
|
+
return status >= 500 ? "server_error" : "api_error";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function unexpectedResponse(operation, body) {
|
|
374
|
+
return new ApiError("unexpected_response", `Unexpected response from ${operation}: ${excerpt(body)}`, { raw: body });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/parse.ts
|
|
378
|
+
function extractByStructure(body, prefix) {
|
|
379
|
+
const entry = new RegExp(
|
|
380
|
+
String.raw`"${prefix}\d+"\s*:\s*\{\s*` + String.raw`"id"\s*:\s*"([^"]*)"\s*,\s*` + String.raw`"name"\s*:\s*"([\s\S]*?)"\s*\}` + String.raw`(?=\s*,\s*"${prefix}\d+"\s*:|\s*\}\s*$)`,
|
|
381
|
+
"g"
|
|
382
|
+
);
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const match of body.matchAll(entry)) {
|
|
385
|
+
out.push({ id: match[1] ?? "", name: match[2] ?? "" });
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
function parseNumberedObject(body, prefix) {
|
|
390
|
+
const trimmed = body.trim();
|
|
391
|
+
if (trimmed === "") return [];
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(trimmed);
|
|
394
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
395
|
+
const out = [];
|
|
396
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
397
|
+
if (!key.startsWith(prefix)) continue;
|
|
398
|
+
if (value === null || typeof value !== "object") continue;
|
|
399
|
+
const row = value;
|
|
400
|
+
out.push({ id: String(row.id ?? ""), name: String(row.name ?? "") });
|
|
401
|
+
}
|
|
402
|
+
return sortByNumericSuffix(out, parsed, prefix);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
const recovered = extractByStructure(trimmed, prefix);
|
|
407
|
+
if (recovered.length > 0) return recovered;
|
|
408
|
+
throw new ApiError(
|
|
409
|
+
"malformed_response",
|
|
410
|
+
`Could not parse the ${prefix} payload returned by the API.`,
|
|
411
|
+
{ raw: body, hint: `The server sent: ${excerpt(trimmed)}` }
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
function sortByNumericSuffix(rows, source, prefix) {
|
|
415
|
+
const keys = Object.keys(source).filter((k) => k.startsWith(prefix));
|
|
416
|
+
const order = /* @__PURE__ */ new Map();
|
|
417
|
+
keys.forEach((key, index) => {
|
|
418
|
+
const n = Number.parseInt(key.slice(prefix.length), 10);
|
|
419
|
+
if (!Number.isNaN(n)) order.set(index, n);
|
|
420
|
+
});
|
|
421
|
+
return rows.map((row, index) => ({ row, sort: order.get(index) ?? index })).sort((a, b) => a.sort - b.sort).map((x) => x.row);
|
|
422
|
+
}
|
|
423
|
+
function parseNumberedRecords(body, prefix) {
|
|
424
|
+
const parsed = parseJsonObject(body, `${prefix} payload`);
|
|
425
|
+
const key = new RegExp(`^${prefix}(\\d+)$`);
|
|
426
|
+
return Object.entries(parsed).map(([name, value]) => ({ match: key.exec(name), value })).filter(
|
|
427
|
+
(entry) => entry.match !== null && entry.value !== null && typeof entry.value === "object"
|
|
428
|
+
).sort((a, b) => Number(a.match[1]) - Number(b.match[1])).map((entry) => entry.value);
|
|
429
|
+
}
|
|
430
|
+
function parseJsonObject(body, what) {
|
|
431
|
+
const trimmed = body.trim();
|
|
432
|
+
try {
|
|
433
|
+
const parsed = JSON.parse(trimmed);
|
|
434
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
throw new ApiError(
|
|
438
|
+
"malformed_response",
|
|
439
|
+
`Could not parse the ${what} returned by the API.`,
|
|
440
|
+
{ raw: body, hint: `The server sent: ${excerpt(trimmed)}` }
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/resources/autoresponders.ts
|
|
445
|
+
var Autoresponders = class {
|
|
446
|
+
constructor(transport) {
|
|
447
|
+
__publicField(this, "transport", transport);
|
|
448
|
+
}
|
|
449
|
+
/** The brand's autoresponders, or one list's. */
|
|
450
|
+
async list(opts = {}) {
|
|
451
|
+
const body = await this.transport.text({
|
|
452
|
+
path: "api/autoresponders/get-autoresponders.php",
|
|
453
|
+
body: { list_id: opts.listId },
|
|
454
|
+
retryable: true
|
|
455
|
+
});
|
|
456
|
+
return parseJsonObject(body, "autoresponders").autoresponders;
|
|
457
|
+
}
|
|
458
|
+
/** One autoresponder with its emails. */
|
|
459
|
+
async get(autoresponderId) {
|
|
460
|
+
const body = await this.transport.text({
|
|
461
|
+
path: "api/autoresponders/get-autoresponder-emails.php",
|
|
462
|
+
body: { autoresponder_id: autoresponderId },
|
|
463
|
+
retryable: true
|
|
464
|
+
});
|
|
465
|
+
return parseJsonObject(body, "autoresponder");
|
|
466
|
+
}
|
|
467
|
+
async emailStats(emailId) {
|
|
468
|
+
const body = await this.transport.text({
|
|
469
|
+
path: "api/autoresponders/autoresponder-stats.php",
|
|
470
|
+
body: { email_id: emailId },
|
|
471
|
+
retryable: true
|
|
472
|
+
});
|
|
473
|
+
return parseJsonObject(body, "autoresponder email stats");
|
|
474
|
+
}
|
|
475
|
+
/** Creates an autoresponder with no emails; add them with `createEmail()`. Not retried. */
|
|
476
|
+
async create(params) {
|
|
477
|
+
const body = await this.transport.text({
|
|
478
|
+
path: "api/autoresponders/create-autoresponder.php",
|
|
479
|
+
body: { name: params.name, type: params.type, list_id: params.listId, custom_field: params.customField }
|
|
480
|
+
});
|
|
481
|
+
return parseJsonObject(body, "autoresponder");
|
|
482
|
+
}
|
|
483
|
+
/** Adds an email to an autoresponder. Not retried: a repeat would add it twice. */
|
|
484
|
+
async createEmail(autoresponderId, params) {
|
|
485
|
+
const body = await this.transport.text({
|
|
486
|
+
path: "api/autoresponders/create-autoresponder-email.php",
|
|
487
|
+
body: {
|
|
488
|
+
autoresponder_id: autoresponderId,
|
|
489
|
+
subject: params.subject,
|
|
490
|
+
html_text: params.htmlText,
|
|
491
|
+
plain_text: params.plainText,
|
|
492
|
+
preheader: params.preheader,
|
|
493
|
+
when: params.when,
|
|
494
|
+
from_name: params.fromName,
|
|
495
|
+
from_email: params.fromEmail,
|
|
496
|
+
reply_to: params.replyTo,
|
|
497
|
+
query_string: params.queryString,
|
|
498
|
+
track_opens: params.trackOpens,
|
|
499
|
+
track_clicks: params.trackClicks,
|
|
500
|
+
enabled: params.enabled ? 1 : void 0
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
return parseJsonObject(body, "autoresponder email");
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/resources/shared.ts
|
|
508
|
+
function joinIds(ids) {
|
|
509
|
+
return ids && ids.length > 0 ? ids.join(",") : void 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/resources/campaigns.ts
|
|
513
|
+
var CAMPAIGN_STATUSES = ["draft", "scheduled", "preparing", "sending", "sent", "paused"];
|
|
514
|
+
var MAX_CAMPAIGNS_PAGE = 100;
|
|
515
|
+
var MAX_TEST_EMAILS = 5;
|
|
516
|
+
var ACTIVITY_TYPES = ["opens", "clicks", "bounces", "complaints", "unsubscribes"];
|
|
517
|
+
var MAX_ACTIVITY_PAGE = 1e3;
|
|
518
|
+
var Campaigns = class {
|
|
519
|
+
constructor(transport) {
|
|
520
|
+
__publicField(this, "transport", transport);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* `api/campaigns/create.php` creates a draft, and optionally sends or
|
|
524
|
+
* schedules it in the same call. To send a draft that already exists, see
|
|
525
|
+
* `send()`.
|
|
526
|
+
*/
|
|
527
|
+
async create(params) {
|
|
528
|
+
const text = await this.transport.text({
|
|
529
|
+
path: "api/campaigns/create.php",
|
|
530
|
+
body: {
|
|
531
|
+
from_name: params.fromName,
|
|
532
|
+
from_email: params.fromEmail,
|
|
533
|
+
reply_to: params.replyTo,
|
|
534
|
+
title: params.title,
|
|
535
|
+
subject: params.subject,
|
|
536
|
+
html_text: params.htmlText,
|
|
537
|
+
plain_text: params.plainText,
|
|
538
|
+
list_ids: joinIds(params.listIds),
|
|
539
|
+
segment_ids: joinIds(params.segmentIds),
|
|
540
|
+
exclude_list_ids: joinIds(params.excludeListIds),
|
|
541
|
+
exclude_segments_ids: joinIds(params.excludeSegmentIds),
|
|
542
|
+
query_string: params.queryString,
|
|
543
|
+
track_opens: params.trackOpens,
|
|
544
|
+
track_clicks: params.trackClicks,
|
|
545
|
+
send_campaign: params.send ? 1 : void 0,
|
|
546
|
+
schedule_date_time: params.scheduleDateTime,
|
|
547
|
+
schedule_timezone: params.scheduleTimezone,
|
|
548
|
+
// Asks for `{"status": ..., "campaign_id": ...}` instead of bare prose.
|
|
549
|
+
json: 1
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const trimmed = text.trim();
|
|
553
|
+
if (trimmed === "Campaign scheduled") return { outcome: "scheduled" };
|
|
554
|
+
if (trimmed === "Campaign created and now sending") return { outcome: "sending" };
|
|
555
|
+
if (trimmed === "Campaign created") return { outcome: "created" };
|
|
556
|
+
if (trimmed.startsWith("{")) {
|
|
557
|
+
try {
|
|
558
|
+
const parsed = JSON.parse(trimmed);
|
|
559
|
+
if (typeof parsed.status === "string" && parsed.status.startsWith("Campaign created")) {
|
|
560
|
+
return { outcome: "created", campaignId: parsed.campaign_id };
|
|
561
|
+
}
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
throw new ApiError("unexpected_response", `Unexpected response from campaigns/create: ${excerpt(trimmed)}`, {
|
|
566
|
+
raw: text
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** One page, newest first. An empty page is an empty array, not an error. */
|
|
570
|
+
async list(params = {}) {
|
|
571
|
+
const body = await this.transport.text({
|
|
572
|
+
path: "api/campaigns/get-campaigns.php",
|
|
573
|
+
body: { page: params.page, limit: params.limit, status: params.status },
|
|
574
|
+
retryable: true
|
|
575
|
+
});
|
|
576
|
+
if (isProseEmpty(body)) return [];
|
|
577
|
+
return parseNumberedRecords(body, "campaign");
|
|
578
|
+
}
|
|
579
|
+
/** Every page, fetched in order at the largest page size. */
|
|
580
|
+
async listAll(params = {}) {
|
|
581
|
+
const all = [];
|
|
582
|
+
for (let page = 1; ; page++) {
|
|
583
|
+
const rows = await this.list({ ...params, page, limit: MAX_CAMPAIGNS_PAGE });
|
|
584
|
+
all.push(...rows);
|
|
585
|
+
if (rows.length < MAX_CAMPAIGNS_PAGE) return all;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async get(campaignId, opts = {}) {
|
|
589
|
+
const body = await this.transport.text({
|
|
590
|
+
path: "api/campaigns/get-campaign.php",
|
|
591
|
+
body: { campaign_id: campaignId, include_content: opts.includeContent ? "yes" : void 0 },
|
|
592
|
+
retryable: true
|
|
593
|
+
});
|
|
594
|
+
return parseJsonObject(body, "campaign");
|
|
595
|
+
}
|
|
596
|
+
async stats(campaignId) {
|
|
597
|
+
const body = await this.transport.text({
|
|
598
|
+
path: "api/campaigns/stats.php",
|
|
599
|
+
body: { campaign_id: campaignId },
|
|
600
|
+
retryable: true
|
|
601
|
+
});
|
|
602
|
+
return parseJsonObject(body, "campaign stats");
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* `api/campaigns/send.php` sends an existing draft. It only ever claims a
|
|
606
|
+
* draft, so a repeat after a lost response answers "Campaign has already been
|
|
607
|
+
* sent" rather than sending twice. It is still not retried: that answer
|
|
608
|
+
* would turn a success into a reported failure.
|
|
609
|
+
*/
|
|
610
|
+
async send(campaignId, recipients) {
|
|
611
|
+
const body = await this.transport.text({
|
|
612
|
+
path: "api/campaigns/send.php",
|
|
613
|
+
body: { campaign_id: campaignId, ...recipientFields(recipients) }
|
|
614
|
+
});
|
|
615
|
+
return parseJsonObject(body, "send result");
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Sends the campaign as a test to up to 5 addresses. Each request counts
|
|
619
|
+
* against a per-brand limit shared with the dashboard (20 an hour), so it is
|
|
620
|
+
* never retried.
|
|
621
|
+
*/
|
|
622
|
+
async testSend(campaignId, emails) {
|
|
623
|
+
const body = await this.transport.text({
|
|
624
|
+
path: "api/campaigns/test-send.php",
|
|
625
|
+
body: { campaign_id: campaignId, emails: emails.join(",") }
|
|
626
|
+
});
|
|
627
|
+
return parseJsonObject(body, "test send result");
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Schedules a draft, or moves a scheduled campaign. The recipients are
|
|
631
|
+
* reserved against the quota now and handed back by `unschedule()`.
|
|
632
|
+
*/
|
|
633
|
+
async schedule(campaignId, params) {
|
|
634
|
+
const body = await this.transport.text({
|
|
635
|
+
path: "api/campaigns/schedule.php",
|
|
636
|
+
body: {
|
|
637
|
+
campaign_id: campaignId,
|
|
638
|
+
...recipientFields(params),
|
|
639
|
+
schedule_date_time: params.at,
|
|
640
|
+
schedule_timezone: params.timezone
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
return parseJsonObject(body, "schedule result");
|
|
644
|
+
}
|
|
645
|
+
/** Back to a draft, with its quota reservation refunded. */
|
|
646
|
+
async unschedule(campaignId) {
|
|
647
|
+
const body = await this.transport.text({ path: "api/campaigns/unschedule.php", body: { campaign_id: campaignId } });
|
|
648
|
+
return parseJsonObject(body, "unschedule result");
|
|
649
|
+
}
|
|
650
|
+
/** Edits a draft or scheduled campaign and returns it as `get()` would. */
|
|
651
|
+
async update(campaignId, changes) {
|
|
652
|
+
const body = await this.transport.text({
|
|
653
|
+
path: "api/campaigns/update-campaign.php",
|
|
654
|
+
body: {
|
|
655
|
+
campaign_id: campaignId,
|
|
656
|
+
title: changes.title,
|
|
657
|
+
subject: changes.subject,
|
|
658
|
+
preheader: changes.preheader,
|
|
659
|
+
from_name: changes.fromName,
|
|
660
|
+
from_email: changes.fromEmail,
|
|
661
|
+
reply_to: changes.replyTo,
|
|
662
|
+
html_text: changes.htmlText,
|
|
663
|
+
plain_text: changes.plainText,
|
|
664
|
+
query_string: changes.queryString,
|
|
665
|
+
track_opens: changes.trackOpens,
|
|
666
|
+
track_clicks: changes.trackClicks
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
return parseJsonObject(body, "campaign");
|
|
670
|
+
}
|
|
671
|
+
/** A new draft copied from the campaign, in the same brand. */
|
|
672
|
+
async duplicate(campaignId, opts = {}) {
|
|
673
|
+
const body = await this.transport.text({
|
|
674
|
+
path: "api/campaigns/duplicate-campaign.php",
|
|
675
|
+
body: { campaign_id: campaignId, title: opts.title }
|
|
676
|
+
});
|
|
677
|
+
return parseJsonObject(body, "campaign");
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Deletes the campaign with its links and attachments; a sent one loses its
|
|
681
|
+
* report. Refused while the campaign is preparing or sending.
|
|
682
|
+
*/
|
|
683
|
+
async delete(campaignId) {
|
|
684
|
+
const body = await this.transport.text({ path: "api/campaigns/delete-campaign.php", body: { campaign_id: campaignId } });
|
|
685
|
+
return parseJsonObject(body, "delete result");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* A page of who opened, clicked, bounced, complained or unsubscribed. Opens
|
|
689
|
+
* and clicks are in the order subscribers first did them.
|
|
690
|
+
*/
|
|
691
|
+
async activity(campaignId, params = {}) {
|
|
692
|
+
const body = await this.transport.text({
|
|
693
|
+
path: "api/campaigns/get-activity.php",
|
|
694
|
+
body: { campaign_id: campaignId, type: params.type, page: params.page, limit: params.limit },
|
|
695
|
+
retryable: true
|
|
696
|
+
});
|
|
697
|
+
return parseJsonObject(body, "campaign activity");
|
|
698
|
+
}
|
|
699
|
+
/** Every page of one activity type, at the largest page size. */
|
|
700
|
+
async activityAll(campaignId, type) {
|
|
701
|
+
const all = [];
|
|
702
|
+
for (let page = 1; ; page++) {
|
|
703
|
+
const r = await this.activity(campaignId, { type, page, limit: MAX_ACTIVITY_PAGE });
|
|
704
|
+
all.push(...r.activity);
|
|
705
|
+
if (r.activity.length === 0 || page * MAX_ACTIVITY_PAGE >= r.total) return all;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
function recipientFields(r) {
|
|
710
|
+
return {
|
|
711
|
+
list_ids: joinIds(r.listIds),
|
|
712
|
+
segment_ids: joinIds(r.segmentIds),
|
|
713
|
+
exclude_list_ids: joinIds(r.excludeListIds),
|
|
714
|
+
exclude_segment_ids: joinIds(r.excludeSegmentIds),
|
|
715
|
+
dry_run: r.dryRun ? 1 : void 0
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/resources/emails.ts
|
|
720
|
+
var MAX_RECIPIENTS = 250;
|
|
721
|
+
var Emails = class {
|
|
722
|
+
constructor(transport) {
|
|
723
|
+
__publicField(this, "transport", transport);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* `api/emails/send.php`: a bulk send to addresses or lists. It creates a
|
|
727
|
+
* campaign behind the scenes; the subject doubles as its title.
|
|
728
|
+
*/
|
|
729
|
+
async send(params) {
|
|
730
|
+
const to = params.to ?? [];
|
|
731
|
+
if (to.length === 0 && (params.listIds ?? []).length === 0) {
|
|
732
|
+
throw new InvalidRequestError("recipients_missing", "Nothing to send to.", {
|
|
733
|
+
hint: "Pass `to` addresses, or `listIds`."
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (to.length > MAX_RECIPIENTS) {
|
|
737
|
+
throw new InvalidRequestError(
|
|
738
|
+
"too_many_recipients",
|
|
739
|
+
`${to.length} recipients is more than the ${MAX_RECIPIENTS} this endpoint accepts.`,
|
|
740
|
+
{ hint: "Split the send, or put the addresses on a list and pass `listIds`." }
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const data = await this.transport.json({
|
|
744
|
+
path: "api/emails/send.php",
|
|
745
|
+
body: {
|
|
746
|
+
from_name: params.fromName,
|
|
747
|
+
from_email: params.fromEmail,
|
|
748
|
+
reply_to: params.replyTo,
|
|
749
|
+
subject: params.subject,
|
|
750
|
+
html_text: params.htmlText,
|
|
751
|
+
plain_text: params.plainText,
|
|
752
|
+
// Both go over the wire as JSON strings inside a form body.
|
|
753
|
+
to: to.length > 0 ? JSON.stringify(to) : void 0,
|
|
754
|
+
"recipient-variables": params.recipientVariables ? JSON.stringify(params.recipientVariables) : void 0,
|
|
755
|
+
list_ids: params.listIds?.join(","),
|
|
756
|
+
query_string: params.queryString,
|
|
757
|
+
track_opens: params.trackOpens,
|
|
758
|
+
track_clicks: params.trackClicks,
|
|
759
|
+
schedule_date_time: params.scheduleDateTime,
|
|
760
|
+
schedule_timezone: params.scheduleTimezone
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
return { message: data.message ?? "Sent.", campaignId: data.data?.campaign_id };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* `api/emails/send-transactional.php`: one email, delivered immediately with
|
|
767
|
+
* nothing queued. For OTPs, password resets and receipts.
|
|
768
|
+
*/
|
|
769
|
+
async sendTransactional(params) {
|
|
770
|
+
if (!params.htmlBody && !params.plainBody) {
|
|
771
|
+
throw new InvalidRequestError("body_missing", "A message body is required.", {
|
|
772
|
+
hint: "Pass `htmlBody`, `plainBody`, or both."
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const data = await this.transport.json({
|
|
776
|
+
path: "api/emails/send-transactional.php",
|
|
777
|
+
encoding: "json",
|
|
778
|
+
body: {
|
|
779
|
+
to_email: params.toEmail,
|
|
780
|
+
subject: params.subject,
|
|
781
|
+
html_body: params.htmlBody,
|
|
782
|
+
plain_body: params.plainBody,
|
|
783
|
+
from_name: params.fromName,
|
|
784
|
+
from_email: params.fromEmail,
|
|
785
|
+
reply_to: params.replyTo
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
return { message: data.message ?? "Email sent successfully.", messageId: data.message_id };
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// src/resources/lists.ts
|
|
793
|
+
var Lists = class {
|
|
794
|
+
constructor(transport) {
|
|
795
|
+
__publicField(this, "transport", transport);
|
|
796
|
+
}
|
|
797
|
+
/** The brand's lists. An empty brand is an empty array, not an error. */
|
|
798
|
+
async list(opts = {}) {
|
|
799
|
+
const body = await this.transport.text({
|
|
800
|
+
path: "api/lists/get-lists.php",
|
|
801
|
+
body: { include_hidden: opts.includeHidden ? "yes" : "no" },
|
|
802
|
+
retryable: true
|
|
803
|
+
});
|
|
804
|
+
if (isProseEmpty(body)) return [];
|
|
805
|
+
return parseNumberedObject(body, "list");
|
|
806
|
+
}
|
|
807
|
+
async get(listId) {
|
|
808
|
+
const body = await this.transport.text({ path: "api/lists/get-list.php", body: { list_id: listId }, retryable: true });
|
|
809
|
+
return parseJsonObject(body, "list");
|
|
810
|
+
}
|
|
811
|
+
/** Not retried: a repeat after a lost response would create a second list. */
|
|
812
|
+
async create(params) {
|
|
813
|
+
const body = await this.transport.text({
|
|
814
|
+
path: "api/lists/create.php",
|
|
815
|
+
body: { name: params.name, opt_in: params.optIn }
|
|
816
|
+
});
|
|
817
|
+
return parseJsonObject(body, "list");
|
|
818
|
+
}
|
|
819
|
+
async update(listId, changes) {
|
|
820
|
+
const body = await this.transport.text({
|
|
821
|
+
path: "api/lists/update-list.php",
|
|
822
|
+
body: { list_id: listId, name: changes.name, opt_in: changes.optIn }
|
|
823
|
+
});
|
|
824
|
+
return parseJsonObject(body, "list");
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Deletes the list with its subscribers, segments, autoresponders and rules.
|
|
828
|
+
* Refused while a scheduled or sending campaign is going to it.
|
|
829
|
+
*/
|
|
830
|
+
async delete(listId) {
|
|
831
|
+
const body = await this.transport.text({ path: "api/lists/delete.php", body: { list_id: listId } });
|
|
832
|
+
return parseJsonObject(body, "delete result");
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Subscribers a campaign to this list would reach.
|
|
836
|
+
*
|
|
837
|
+
* Anything but a bare integer is a failure and is thrown, never reported as
|
|
838
|
+
* 0: "this list is empty" is the one wrong answer a caller cannot detect.
|
|
839
|
+
*/
|
|
840
|
+
async activeSubscriberCount(listId) {
|
|
841
|
+
const body = await this.transport.text({
|
|
842
|
+
path: "api/subscribers/active-subscriber-count.php",
|
|
843
|
+
body: { list_id: listId },
|
|
844
|
+
retryable: true
|
|
845
|
+
});
|
|
846
|
+
const trimmed = body.trim();
|
|
847
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
848
|
+
throw new ApiError("malformed_response", `Expected a subscriber count for list ${listId}, got: ${excerpt(trimmed)}`, {
|
|
849
|
+
raw: body
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return Number.parseInt(trimmed, 10);
|
|
853
|
+
}
|
|
854
|
+
/** Adds a field; every subscriber gets an empty value for it. Returns the list with its fields. */
|
|
855
|
+
async addCustomField(listId, params) {
|
|
856
|
+
const body = await this.transport.text({
|
|
857
|
+
path: "api/lists/add-custom-field.php",
|
|
858
|
+
body: { list_id: listId, name: params.name, type: params.type }
|
|
859
|
+
});
|
|
860
|
+
return parseJsonObject(body, "list");
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Renames a field, keeping subscribers' values. The autoresponders and
|
|
864
|
+
* segment conditions that use it follow; tags already in campaigns keep the
|
|
865
|
+
* old name.
|
|
866
|
+
*/
|
|
867
|
+
async renameCustomField(listId, name, newName) {
|
|
868
|
+
const body = await this.transport.text({
|
|
869
|
+
path: "api/lists/rename-custom-field.php",
|
|
870
|
+
body: { list_id: listId, name, new_name: newName }
|
|
871
|
+
});
|
|
872
|
+
return parseJsonObject(body, "list");
|
|
873
|
+
}
|
|
874
|
+
/** Deletes a field and every subscriber's value for it. Refused while an autoresponder or segment uses it. */
|
|
875
|
+
async deleteCustomField(listId, name) {
|
|
876
|
+
const body = await this.transport.text({
|
|
877
|
+
path: "api/lists/delete-custom-field.php",
|
|
878
|
+
body: { list_id: listId, name }
|
|
879
|
+
});
|
|
880
|
+
return parseJsonObject(body, "list");
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// src/resources/misc.ts
|
|
885
|
+
var Brands = class {
|
|
886
|
+
constructor(transport) {
|
|
887
|
+
__publicField(this, "transport", transport);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* The key's own brand: a brand key gets exactly one row. Empty, not an error,
|
|
891
|
+
* when there is none.
|
|
892
|
+
*/
|
|
893
|
+
async list() {
|
|
894
|
+
const body = await this.transport.text({ path: "api/brands/get-brands.php", retryable: true });
|
|
895
|
+
if (isProseEmpty(body)) return [];
|
|
896
|
+
return parseNumberedObject(body, "brand");
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var Stats = class {
|
|
900
|
+
constructor(transport) {
|
|
901
|
+
__publicField(this, "transport", transport);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Emails sent across the whole installation (campaigns, autoresponders and
|
|
905
|
+
* transactional), not a per-brand figure. Public and cached for 5 minutes.
|
|
906
|
+
*/
|
|
907
|
+
async totalEmailsSent() {
|
|
908
|
+
const data = await this.transport.json({
|
|
909
|
+
path: "api/stats/emails-sent.php",
|
|
910
|
+
method: "GET",
|
|
911
|
+
anonymous: true,
|
|
912
|
+
retryable: true
|
|
913
|
+
});
|
|
914
|
+
return data.total_emails_sent ?? 0;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// src/resources/subscribers.ts
|
|
919
|
+
var SUBSCRIBER_STATUSES = ["active", "unconfirmed", "unsubscribed", "bounced", "complained"];
|
|
920
|
+
var MAX_SUBSCRIBERS_PAGE = 1e3;
|
|
921
|
+
var Subscribers = class {
|
|
922
|
+
constructor(transport) {
|
|
923
|
+
__publicField(this, "transport", transport);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Adds an address to a list. Posting one that is already there updates its
|
|
927
|
+
* name and custom fields, and resolves `already_subscribed` rather than
|
|
928
|
+
* throwing, so re-running an import is safe.
|
|
929
|
+
*/
|
|
930
|
+
async subscribe(params) {
|
|
931
|
+
const body = {};
|
|
932
|
+
for (const [name, value] of Object.entries(params.fields ?? {})) body[name.replace(/ /g, "")] = value;
|
|
933
|
+
body.email = params.email;
|
|
934
|
+
body.list = params.listId;
|
|
935
|
+
body.boolean = "true";
|
|
936
|
+
if (params.name !== void 0) body.name = params.name;
|
|
937
|
+
if (params.country !== void 0) body.country = params.country;
|
|
938
|
+
if (params.ipAddress !== void 0) body.ipaddress = params.ipAddress;
|
|
939
|
+
if (params.referrer !== void 0) body.referrer = params.referrer;
|
|
940
|
+
if (params.gdpr) body.gdpr = "true";
|
|
941
|
+
if (params.silent) body.silent = "true";
|
|
942
|
+
const text = await this.transport.text({ path: "subscribe", body });
|
|
943
|
+
if (text.trim().toLowerCase() === "already subscribed.") return "already_subscribed";
|
|
944
|
+
if (isProseSuccess(text)) return "subscribed";
|
|
945
|
+
throw unexpectedResponse("subscribe", text);
|
|
946
|
+
}
|
|
947
|
+
/** Marks the address unsubscribed from the list. */
|
|
948
|
+
async unsubscribe(ref) {
|
|
949
|
+
const text = await this.transport.text({
|
|
950
|
+
path: "unsubscribe",
|
|
951
|
+
body: { email: ref.email, list: ref.listId, boolean: "true" }
|
|
952
|
+
});
|
|
953
|
+
if (!isProseSuccess(text)) throw unexpectedResponse("unsubscribe", text);
|
|
954
|
+
}
|
|
955
|
+
/** Removes the subscriber outright, rather than marking it unsubscribed. */
|
|
956
|
+
async delete(ref) {
|
|
957
|
+
const text = await this.transport.text({
|
|
958
|
+
path: "api/subscribers/delete.php",
|
|
959
|
+
body: { list_id: ref.listId, email: ref.email }
|
|
960
|
+
});
|
|
961
|
+
if (!isProseSuccess(text)) throw unexpectedResponse("delete", text);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* One of Subscribed, Unsubscribed, Unconfirmed, Bounced, Soft bounced or
|
|
965
|
+
* Complained. Returned as given, so a status added server-side surfaces
|
|
966
|
+
* rather than being rejected.
|
|
967
|
+
*/
|
|
968
|
+
async status(ref) {
|
|
969
|
+
const text = await this.transport.text({
|
|
970
|
+
path: "api/subscribers/subscription-status.php",
|
|
971
|
+
body: { email: ref.email, list_id: ref.listId },
|
|
972
|
+
retryable: true
|
|
973
|
+
});
|
|
974
|
+
const status = text.trim();
|
|
975
|
+
if (status === "") throw unexpectedResponse("subscription-status", text);
|
|
976
|
+
return status;
|
|
977
|
+
}
|
|
978
|
+
/** One page of a list's subscribers, oldest first. */
|
|
979
|
+
async list(listId, params = {}) {
|
|
980
|
+
const body = await this.transport.text({
|
|
981
|
+
path: "api/subscribers/get-subscribers.php",
|
|
982
|
+
body: { list_id: listId, status: params.status, page: params.page, limit: params.limit },
|
|
983
|
+
retryable: true
|
|
984
|
+
});
|
|
985
|
+
return parseJsonObject(body, "subscribers page");
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Every page, at the largest page size. Oldest first, so someone joining
|
|
989
|
+
* mid-way lands on a later page instead of shifting the ones already read.
|
|
990
|
+
*/
|
|
991
|
+
async listAll(listId, params = {}) {
|
|
992
|
+
const all = [];
|
|
993
|
+
for (let page = 1; ; page++) {
|
|
994
|
+
const r = await this.list(listId, { ...params, page, limit: MAX_SUBSCRIBERS_PAGE });
|
|
995
|
+
all.push(...r.subscribers);
|
|
996
|
+
if (r.subscribers.length < MAX_SUBSCRIBERS_PAGE) return all;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/resources/templates.ts
|
|
1002
|
+
var fieldsBody = (fields) => ({
|
|
1003
|
+
name: fields.name,
|
|
1004
|
+
html_text: fields.htmlText,
|
|
1005
|
+
plain_text: fields.plainText,
|
|
1006
|
+
from_name: fields.fromName,
|
|
1007
|
+
from_email: fields.fromEmail,
|
|
1008
|
+
reply_to: fields.replyTo,
|
|
1009
|
+
editor: fields.editor
|
|
1010
|
+
});
|
|
1011
|
+
var Templates = class {
|
|
1012
|
+
constructor(transport) {
|
|
1013
|
+
__publicField(this, "transport", transport);
|
|
1014
|
+
}
|
|
1015
|
+
/** The brand's templates by name, without their HTML. */
|
|
1016
|
+
async list() {
|
|
1017
|
+
const text = await this.transport.text({ path: "api/templates/get-templates.php", retryable: true });
|
|
1018
|
+
return parseJsonObject(text, "templates").templates;
|
|
1019
|
+
}
|
|
1020
|
+
async get(templateId, opts = {}) {
|
|
1021
|
+
const text = await this.transport.text({
|
|
1022
|
+
path: "api/templates/get-template.php",
|
|
1023
|
+
body: { template_id: templateId, include_content: opts.includeContent ? "yes" : void 0 },
|
|
1024
|
+
retryable: true
|
|
1025
|
+
});
|
|
1026
|
+
return parseJsonObject(text, "template");
|
|
1027
|
+
}
|
|
1028
|
+
/** Not retried: a repeat would create a second template. */
|
|
1029
|
+
async create(fields) {
|
|
1030
|
+
const text = await this.transport.text({ path: "api/templates/create-template.php", body: fieldsBody(fields) });
|
|
1031
|
+
return parseJsonObject(text, "template");
|
|
1032
|
+
}
|
|
1033
|
+
/** Only the fields given change. */
|
|
1034
|
+
async update(templateId, fields) {
|
|
1035
|
+
const text = await this.transport.text({
|
|
1036
|
+
path: "api/templates/update-template.php",
|
|
1037
|
+
body: { template_id: templateId, ...fieldsBody(fields) }
|
|
1038
|
+
});
|
|
1039
|
+
return parseJsonObject(text, "template");
|
|
1040
|
+
}
|
|
1041
|
+
/** Campaigns already made from it are unaffected. */
|
|
1042
|
+
async delete(templateId) {
|
|
1043
|
+
const text = await this.transport.text({
|
|
1044
|
+
path: "api/templates/delete-template.php",
|
|
1045
|
+
body: { template_id: templateId }
|
|
1046
|
+
});
|
|
1047
|
+
return parseJsonObject(text, "delete result");
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/version.ts
|
|
1052
|
+
var VERSION = "2.0.0";
|
|
1053
|
+
|
|
1054
|
+
// src/transport.ts
|
|
1055
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
1056
|
+
var Transport = class {
|
|
1057
|
+
constructor(opts) {
|
|
1058
|
+
__publicField(this, "opts");
|
|
1059
|
+
this.opts = { ...opts, baseUrl: opts.baseUrl.replace(/\/+$/, "") };
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Perform a request and return the body as text.
|
|
1063
|
+
*
|
|
1064
|
+
* Legacy endpoints answer HTTP 200 with a prose error, so a 2xx here is not
|
|
1065
|
+
* proof of success: recognised prose errors are thrown, and anything else is
|
|
1066
|
+
* handed back for the caller to parse.
|
|
1067
|
+
*/
|
|
1068
|
+
async text(opts) {
|
|
1069
|
+
const response = await this.send(opts);
|
|
1070
|
+
const text = await response.text();
|
|
1071
|
+
if (!response.ok) throw errorFromResponse(response, text);
|
|
1072
|
+
const prose = classifyProse(text, response.status);
|
|
1073
|
+
if (prose) throw prose;
|
|
1074
|
+
return text;
|
|
1075
|
+
}
|
|
1076
|
+
/** Perform a request against one of the endpoints that returns real JSON. */
|
|
1077
|
+
async json(opts) {
|
|
1078
|
+
const response = await this.send(opts);
|
|
1079
|
+
const text = await response.text();
|
|
1080
|
+
if (!response.ok) throw errorFromResponse(response, text);
|
|
1081
|
+
try {
|
|
1082
|
+
return JSON.parse(text);
|
|
1083
|
+
} catch {
|
|
1084
|
+
throw new ApiError("malformed_response", "The API returned a response that was not valid JSON.", {
|
|
1085
|
+
raw: text,
|
|
1086
|
+
status: response.status
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async send(opts) {
|
|
1091
|
+
const method = opts.method ?? "POST";
|
|
1092
|
+
const target = `${this.opts.baseUrl}/${opts.path.replace(/^\/+/, "")}`;
|
|
1093
|
+
const fields = {};
|
|
1094
|
+
for (const [key, value] of Object.entries(opts.body ?? {})) {
|
|
1095
|
+
if (value !== void 0) fields[key] = String(value);
|
|
1096
|
+
}
|
|
1097
|
+
if (!opts.anonymous) fields.api_key = this.opts.apiKey;
|
|
1098
|
+
const headers = { "User-Agent": `skrybe-sdk-ts/${VERSION}` };
|
|
1099
|
+
let url = target;
|
|
1100
|
+
let body;
|
|
1101
|
+
if (method === "GET") {
|
|
1102
|
+
const query = new URLSearchParams(fields).toString();
|
|
1103
|
+
if (query !== "") url = `${target}?${query}`;
|
|
1104
|
+
} else if (opts.encoding === "json") {
|
|
1105
|
+
headers["Content-Type"] = "application/json";
|
|
1106
|
+
body = JSON.stringify(fields);
|
|
1107
|
+
} else {
|
|
1108
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1109
|
+
body = new URLSearchParams(fields).toString();
|
|
1110
|
+
}
|
|
1111
|
+
const attempts = opts.retryable ? this.opts.maxRetries + 1 : 1;
|
|
1112
|
+
let lastError;
|
|
1113
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1114
|
+
const controller = new AbortController();
|
|
1115
|
+
const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs);
|
|
1116
|
+
try {
|
|
1117
|
+
const response = await this.opts.fetch(url, { method, headers, body, signal: controller.signal });
|
|
1118
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < attempts) {
|
|
1119
|
+
await response.text().catch(() => void 0);
|
|
1120
|
+
await delay(backoffMs(attempt, response.headers.get("retry-after")));
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
return response;
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
lastError = err;
|
|
1126
|
+
if (isAbort(err)) {
|
|
1127
|
+
throw new ConnectionError("timeout", `Request to ${target} timed out after ${this.opts.timeoutMs}ms.`, {
|
|
1128
|
+
cause: err
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
if (attempt < attempts) {
|
|
1132
|
+
await delay(backoffMs(attempt, null));
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
} finally {
|
|
1136
|
+
clearTimeout(timer);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
throw new ConnectionError(
|
|
1140
|
+
"network_error",
|
|
1141
|
+
`Could not reach ${this.opts.baseUrl}: ${lastError?.message ?? "unknown error"}`,
|
|
1142
|
+
{ cause: lastError }
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
function errorFromResponse(response, text) {
|
|
1147
|
+
let message = text.trim();
|
|
1148
|
+
try {
|
|
1149
|
+
const parsed = JSON.parse(text);
|
|
1150
|
+
const extracted = parsed.error ?? parsed.message;
|
|
1151
|
+
if (typeof extracted === "string" && extracted !== "") message = extracted;
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
if (message === "") message = `HTTP ${response.status} from the Skrybe API.`;
|
|
1155
|
+
return createError(typeForStatus(response.status), codeForStatus(response.status), message, {
|
|
1156
|
+
raw: text,
|
|
1157
|
+
status: response.status
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function backoffMs(attempt, retryAfter) {
|
|
1161
|
+
if (retryAfter) {
|
|
1162
|
+
const seconds = Number.parseInt(retryAfter, 10);
|
|
1163
|
+
if (!Number.isNaN(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 3e4);
|
|
1164
|
+
}
|
|
1165
|
+
return 2 ** (attempt - 1) * 500 + Math.floor(Math.random() * 250);
|
|
1166
|
+
}
|
|
1167
|
+
function delay(ms) {
|
|
1168
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1169
|
+
}
|
|
1170
|
+
function isAbort(err) {
|
|
1171
|
+
return err instanceof Error && err.name === "AbortError";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/client.ts
|
|
1175
|
+
var DEFAULT_BASE_URL = "https://dashboard.skry.be";
|
|
1176
|
+
var Skrybe = class {
|
|
1177
|
+
constructor(options) {
|
|
1178
|
+
__publicField(this, "emails");
|
|
1179
|
+
__publicField(this, "campaigns");
|
|
1180
|
+
__publicField(this, "lists");
|
|
1181
|
+
__publicField(this, "subscribers");
|
|
1182
|
+
__publicField(this, "templates");
|
|
1183
|
+
__publicField(this, "autoresponders");
|
|
1184
|
+
__publicField(this, "brands");
|
|
1185
|
+
__publicField(this, "stats");
|
|
1186
|
+
if (!options?.apiKey) {
|
|
1187
|
+
throw new InvalidRequestError("api_key_missing", "An apiKey is required.", {
|
|
1188
|
+
hint: "Generate one in the Skrybe dashboard under Settings."
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1192
|
+
if (typeof fetchImpl !== "function") {
|
|
1193
|
+
throw new InvalidRequestError("fetch_unavailable", "No global fetch is available in this runtime.", {
|
|
1194
|
+
hint: "Use Node.js 18 or newer, or pass a fetch implementation as `fetch`."
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
const transport = new Transport({
|
|
1198
|
+
apiKey: options.apiKey,
|
|
1199
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
1200
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
1201
|
+
maxRetries: Math.max(0, options.maxRetries ?? 2),
|
|
1202
|
+
// Bound so a fetch that relies on `this` being globalThis still works.
|
|
1203
|
+
fetch: options.fetch ?? ((input, init) => globalThis.fetch(input, init))
|
|
1204
|
+
});
|
|
1205
|
+
this.emails = new Emails(transport);
|
|
1206
|
+
this.campaigns = new Campaigns(transport);
|
|
1207
|
+
this.lists = new Lists(transport);
|
|
1208
|
+
this.subscribers = new Subscribers(transport);
|
|
1209
|
+
this.templates = new Templates(transport);
|
|
1210
|
+
this.autoresponders = new Autoresponders(transport);
|
|
1211
|
+
this.brands = new Brands(transport);
|
|
1212
|
+
this.stats = new Stats(transport);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1216
|
+
0 && (module.exports = {
|
|
1217
|
+
ACTIVITY_TYPES,
|
|
1218
|
+
ApiError,
|
|
1219
|
+
AuthenticationError,
|
|
1220
|
+
CAMPAIGN_STATUSES,
|
|
1221
|
+
ConnectionError,
|
|
1222
|
+
DEFAULT_BASE_URL,
|
|
1223
|
+
InvalidRequestError,
|
|
1224
|
+
MAX_ACTIVITY_PAGE,
|
|
1225
|
+
MAX_CAMPAIGNS_PAGE,
|
|
1226
|
+
MAX_RECIPIENTS,
|
|
1227
|
+
MAX_SUBSCRIBERS_PAGE,
|
|
1228
|
+
MAX_TEST_EMAILS,
|
|
1229
|
+
RateLimitError,
|
|
1230
|
+
SUBSCRIBER_STATUSES,
|
|
1231
|
+
Skrybe,
|
|
1232
|
+
SkrybeError,
|
|
1233
|
+
VERSION
|
|
1234
|
+
});
|
|
1235
|
+
//# sourceMappingURL=index.cjs.map
|