@develemail/sdk 0.1.0 → 0.4.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.js CHANGED
@@ -1,50 +1,598 @@
1
- // src/index.ts
1
+ // src/errors.ts
2
2
  var DevelEmailError = class extends Error {
3
- constructor(status, message) {
3
+ constructor(status, message, code, details) {
4
4
  super(message);
5
5
  this.status = status;
6
+ this.code = code;
7
+ this.details = details;
6
8
  this.name = "DevelEmailError";
7
9
  }
8
10
  status;
11
+ code;
12
+ details;
9
13
  };
10
- var EmailsNamespace = class {
14
+
15
+ // src/client.ts
16
+ var DEFAULT_MAX_RETRIES = 3;
17
+ var BASE_DELAY_MS = 500;
18
+ var MAX_DELAY_MS = 3e4;
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ function isRetryable(status) {
23
+ return status >= 500 || status === 429;
24
+ }
25
+ function parseErrorBody(body) {
26
+ if (typeof body === "object" && body !== null) {
27
+ const b = body;
28
+ const { message, error, statusCode, ...rest } = b;
29
+ return {
30
+ message: typeof message === "string" ? message : "Unknown error",
31
+ code: typeof error === "string" ? error : void 0,
32
+ details: Object.keys(rest).length > 0 ? rest : void 0
33
+ };
34
+ }
35
+ return { message: "Unknown error" };
36
+ }
37
+ var HttpClient = class {
38
+ baseUrl;
39
+ apiKey;
40
+ maxRetries;
11
41
  constructor(config) {
12
- this.config = config;
42
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
43
+ this.apiKey = config.apiKey;
44
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
13
45
  }
14
- config;
15
- async send(input) {
16
- return this.request("POST", "/v1/emails", input);
46
+ async get(path) {
47
+ return this.request("GET", path);
17
48
  }
18
- async get(id) {
19
- return this.request("GET", `/v1/emails/${id}`);
49
+ async post(path, body, extraHeaders) {
50
+ return this.request("POST", path, body, extraHeaders);
51
+ }
52
+ async put(path, body, extraHeaders) {
53
+ return this.request("PUT", path, body, extraHeaders);
54
+ }
55
+ async patch(path, body, extraHeaders) {
56
+ return this.request("PATCH", path, body, extraHeaders);
20
57
  }
21
- async request(method, path, body) {
22
- const res = await fetch(`${this.config.baseUrl}${path}`, {
58
+ async delete(path, extraHeaders) {
59
+ return this.request("DELETE", path, void 0, extraHeaders);
60
+ }
61
+ async request(method, path, body, extraHeaders) {
62
+ const url = `${this.baseUrl}${path}`;
63
+ const headers = {
64
+ Authorization: `Bearer ${this.apiKey}`,
65
+ "Content-Type": "application/json",
66
+ ...extraHeaders
67
+ };
68
+ const init = {
23
69
  method,
24
- headers: {
25
- Authorization: `Bearer ${this.config.apiKey}`,
26
- "Content-Type": "application/json"
27
- },
70
+ headers,
28
71
  body: body !== void 0 ? JSON.stringify(body) : void 0
29
- });
30
- if (!res.ok) {
31
- const data = await res.json().catch(() => ({}));
72
+ };
73
+ let lastError;
74
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
75
+ let res;
76
+ try {
77
+ res = await fetch(url, init);
78
+ } catch (err) {
79
+ lastError = err;
80
+ if (attempt < this.maxRetries) {
81
+ await sleep(this.backoff(attempt));
82
+ continue;
83
+ }
84
+ throw new DevelEmailError(
85
+ 0,
86
+ err instanceof Error ? err.message : "Network error",
87
+ "network_error"
88
+ );
89
+ }
90
+ if (res.ok) {
91
+ if (res.status === 204) return void 0;
92
+ return res.json();
93
+ }
94
+ const errorBody = await res.json().catch(() => ({}));
95
+ const parsed = parseErrorBody(errorBody);
96
+ if (isRetryable(res.status) && attempt < this.maxRetries) {
97
+ const retryAfter = res.headers.get("Retry-After");
98
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : this.backoff(attempt);
99
+ await sleep(delay);
100
+ lastError = new DevelEmailError(
101
+ res.status,
102
+ parsed.message,
103
+ parsed.code,
104
+ parsed.details
105
+ );
106
+ continue;
107
+ }
32
108
  throw new DevelEmailError(
33
109
  res.status,
34
- data.message ?? res.statusText
110
+ parsed.message,
111
+ parsed.code,
112
+ parsed.details
35
113
  );
36
114
  }
37
- return res.json();
115
+ if (lastError instanceof DevelEmailError) throw lastError;
116
+ throw new DevelEmailError(
117
+ 0,
118
+ lastError instanceof Error ? lastError.message : "Request failed",
119
+ "network_error"
120
+ );
121
+ }
122
+ backoff(attempt) {
123
+ const delay = BASE_DELAY_MS * 2 ** attempt;
124
+ const jitter = delay * 0.2 * Math.random();
125
+ return Math.min(delay + jitter, MAX_DELAY_MS);
126
+ }
127
+ };
128
+
129
+ // ../core/src/reserved-domains.ts
130
+ var RESERVED_DOMAINS = ["example.com", "example.net", "example.org"];
131
+ var RESERVED_TLDS = ["test", "example", "invalid", "localhost"];
132
+ function isReservedDomain(domain) {
133
+ const normalized = domain.trim().toLowerCase().replace(/\.$/, "");
134
+ if (!normalized) return false;
135
+ if (RESERVED_DOMAINS.includes(normalized)) return true;
136
+ const tld = normalized.split(".").pop() ?? "";
137
+ return RESERVED_TLDS.includes(tld);
138
+ }
139
+
140
+ // src/attachment-encoding.ts
141
+ function encodeAttachmentContent(content) {
142
+ return typeof content === "string" ? content : Buffer.from(content).toString("base64");
143
+ }
144
+ function encodeAttachments(attachments) {
145
+ if (!attachments) return void 0;
146
+ return attachments.map((attachment) => ({
147
+ filename: attachment.filename,
148
+ contentType: attachment.contentType,
149
+ content: encodeAttachmentContent(attachment.content)
150
+ }));
151
+ }
152
+
153
+ // src/emails.ts
154
+ function toWirePayload(input) {
155
+ return { ...input, attachments: encodeAttachments(input.attachments) };
156
+ }
157
+ function extractDomain(email) {
158
+ return email.split("@")[1] ?? "";
159
+ }
160
+ function reservedRecipientError(recipient) {
161
+ const domain = extractDomain(recipient);
162
+ return new DevelEmailError(
163
+ 400,
164
+ `Recipient '${recipient}' uses reserved domain '${domain}', which cannot receive mail`,
165
+ "reserved_recipient_domain",
166
+ { recipient, domain }
167
+ );
168
+ }
169
+ function findReservedRecipient(input) {
170
+ const toAddresses = Array.isArray(input.to) ? input.to : [input.to];
171
+ return toAddresses.find((addr) => isReservedDomain(extractDomain(addr))) ?? null;
172
+ }
173
+ var Emails = class {
174
+ constructor(http) {
175
+ this.http = http;
176
+ }
177
+ http;
178
+ async send(input, options) {
179
+ const reserved = findReservedRecipient(input);
180
+ if (reserved) throw reservedRecipientError(reserved);
181
+ const key = options?.idempotencyKey ?? crypto.randomUUID();
182
+ return this.http.post("/v1/emails", toWirePayload(input), {
183
+ "Idempotency-Key": key
184
+ });
185
+ }
186
+ // Rejects reserved recipients client-side but, matching the server's
187
+ // per-item batch semantics, does not fail the whole batch: deliverable
188
+ // items still go out, and each reserved item is named in `results` with
189
+ // an `error` instead of being silently dropped.
190
+ async batch(emails, options) {
191
+ const rejections = /* @__PURE__ */ new Map();
192
+ emails.forEach((email, index) => {
193
+ const reserved = findReservedRecipient(email);
194
+ if (reserved) {
195
+ rejections.set(index, {
196
+ index,
197
+ error: reservedRecipientError(reserved).message
198
+ });
199
+ }
200
+ });
201
+ const deliverable = emails.filter((_, index) => !rejections.has(index));
202
+ if (deliverable.length === 0) {
203
+ return {
204
+ results: emails.map((_, index) => rejections.get(index)),
205
+ created: 0,
206
+ errors: rejections.size
207
+ };
208
+ }
209
+ const key = options?.idempotencyKey ?? crypto.randomUUID();
210
+ const response = await this.http.post(
211
+ "/v1/emails/batch",
212
+ { emails: deliverable.map(toWirePayload) },
213
+ { "Idempotency-Key": key }
214
+ );
215
+ const results = [];
216
+ let sent = 0;
217
+ for (let index = 0; index < emails.length; index++) {
218
+ const rejection = rejections.get(index);
219
+ results.push(rejection ?? { ...response.results[sent++], index });
220
+ }
221
+ return {
222
+ results,
223
+ created: response.created,
224
+ errors: response.errors + rejections.size,
225
+ limitReached: response.limitReached
226
+ };
227
+ }
228
+ async get(id) {
229
+ return this.http.get(`/v1/emails/${encodeURIComponent(id)}`);
230
+ }
231
+ };
232
+
233
+ // src/domains.ts
234
+ var Domains = class {
235
+ constructor(http) {
236
+ this.http = http;
237
+ }
238
+ http;
239
+ async list() {
240
+ return this.http.get("/domains");
241
+ }
242
+ async get(id) {
243
+ return this.http.get(`/domains/${encodeURIComponent(id)}`);
244
+ }
245
+ async create(input) {
246
+ return this.http.post("/domains", input);
247
+ }
248
+ async delete(id) {
249
+ await this.http.delete(`/domains/${encodeURIComponent(id)}`);
250
+ }
251
+ async verify(id) {
252
+ return this.http.post(
253
+ `/domains/${encodeURIComponent(id)}/verify`,
254
+ void 0
255
+ );
256
+ }
257
+ };
258
+
259
+ // src/templates.ts
260
+ var Templates = class {
261
+ constructor(http) {
262
+ this.http = http;
263
+ }
264
+ http;
265
+ async list() {
266
+ return this.http.get("/templates");
267
+ }
268
+ async get(id) {
269
+ return this.http.get(`/templates/${encodeURIComponent(id)}`);
270
+ }
271
+ async create(input) {
272
+ return this.http.post("/templates", input);
273
+ }
274
+ async update(id, input) {
275
+ return this.http.put(
276
+ `/templates/${encodeURIComponent(id)}`,
277
+ input
278
+ );
279
+ }
280
+ async delete(id) {
281
+ await this.http.delete(`/templates/${encodeURIComponent(id)}`);
282
+ }
283
+ };
284
+
285
+ // src/campaigns.ts
286
+ function toQueryString(query) {
287
+ const params = new URLSearchParams();
288
+ params.set("projectId", query.projectId);
289
+ if (query.status) params.set("status", query.status);
290
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
291
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
292
+ return params.toString();
293
+ }
294
+ var Campaigns = class {
295
+ constructor(http) {
296
+ this.http = http;
297
+ }
298
+ http;
299
+ async list(query) {
300
+ return this.http.get(`/campaigns?${toQueryString(query)}`);
301
+ }
302
+ async get(id) {
303
+ return this.http.get(`/campaigns/${encodeURIComponent(id)}`);
304
+ }
305
+ async create(input) {
306
+ return this.http.post("/campaigns", input);
307
+ }
308
+ async update(id, input) {
309
+ return this.http.patch(
310
+ `/campaigns/${encodeURIComponent(id)}`,
311
+ input
312
+ );
313
+ }
314
+ async send(id) {
315
+ return this.http.post(
316
+ `/campaigns/${encodeURIComponent(id)}/send`,
317
+ void 0
318
+ );
319
+ }
320
+ async pause(id) {
321
+ return this.http.post(
322
+ `/campaigns/${encodeURIComponent(id)}/pause`,
323
+ void 0
324
+ );
325
+ }
326
+ async resume(id) {
327
+ return this.http.post(
328
+ `/campaigns/${encodeURIComponent(id)}/resume`,
329
+ void 0
330
+ );
331
+ }
332
+ async cancel(id) {
333
+ return this.http.post(
334
+ `/campaigns/${encodeURIComponent(id)}/cancel`,
335
+ void 0
336
+ );
337
+ }
338
+ async duplicate(id) {
339
+ return this.http.post(
340
+ `/campaigns/${encodeURIComponent(id)}/duplicate`,
341
+ void 0
342
+ );
343
+ }
344
+ async delete(id) {
345
+ await this.http.delete(`/campaigns/${encodeURIComponent(id)}`);
346
+ }
347
+ async stats(id) {
348
+ return this.http.get(
349
+ `/campaigns/${encodeURIComponent(id)}/stats`
350
+ );
351
+ }
352
+ async providerStats(id) {
353
+ return this.http.get(
354
+ `/campaigns/${encodeURIComponent(id)}/provider-stats`
355
+ );
356
+ }
357
+ };
358
+
359
+ // src/contacts.ts
360
+ function toQueryString2(query) {
361
+ const params = new URLSearchParams();
362
+ params.set("projectId", query.projectId);
363
+ if (query.search) params.set("search", query.search);
364
+ if (query.tags) {
365
+ const tags = query.tags.filter(Boolean);
366
+ if (tags.length > 0) params.set("tags", tags.join(","));
367
+ }
368
+ if (query.tagMode) params.set("tagMode", query.tagMode);
369
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
370
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
371
+ return params.toString();
372
+ }
373
+ var Contacts = class {
374
+ constructor(http) {
375
+ this.http = http;
376
+ }
377
+ http;
378
+ async list(query) {
379
+ return this.http.get(
380
+ `/contacts?${toQueryString2(query)}`
381
+ );
382
+ }
383
+ async get(id) {
384
+ return this.http.get(`/contacts/${encodeURIComponent(id)}`);
385
+ }
386
+ async create(input) {
387
+ return this.http.post("/contacts", input);
388
+ }
389
+ async update(id, input) {
390
+ return this.http.patch(
391
+ `/contacts/${encodeURIComponent(id)}`,
392
+ input
393
+ );
394
+ }
395
+ async delete(id) {
396
+ await this.http.delete(`/contacts/${encodeURIComponent(id)}`);
397
+ }
398
+ async bulkCreate(input) {
399
+ return this.http.post("/contacts/bulk", input);
400
+ }
401
+ async tags(projectId) {
402
+ const params = new URLSearchParams();
403
+ params.set("projectId", projectId);
404
+ return this.http.get(`/contacts/tags?${params.toString()}`);
405
+ }
406
+ async lists(contactId) {
407
+ return this.http.get(
408
+ `/contacts/${encodeURIComponent(contactId)}/lists`
409
+ );
410
+ }
411
+ /** @deprecated Use `lists.addContacts` instead. */
412
+ async addToList(contactId, listId) {
413
+ await this.http.post(
414
+ `/lists/${encodeURIComponent(listId)}/contacts`,
415
+ { contactIds: [contactId] }
416
+ );
417
+ }
418
+ /** @deprecated Use `lists.removeContact` instead. */
419
+ async removeFromList(contactId, listId) {
420
+ await this.http.delete(
421
+ `/lists/${encodeURIComponent(listId)}/contacts/${encodeURIComponent(contactId)}`
422
+ );
38
423
  }
39
424
  };
425
+
426
+ // src/lists.ts
427
+ function toQueryString3(query) {
428
+ const params = new URLSearchParams();
429
+ if (query.search) params.set("search", query.search);
430
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
431
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
432
+ return params.toString();
433
+ }
434
+ var Lists = class {
435
+ constructor(http) {
436
+ this.http = http;
437
+ }
438
+ http;
439
+ async list(projectId) {
440
+ const params = new URLSearchParams();
441
+ params.set("projectId", projectId);
442
+ return this.http.get(`/lists?${params.toString()}`);
443
+ }
444
+ async get(id) {
445
+ return this.http.get(`/lists/${encodeURIComponent(id)}`);
446
+ }
447
+ async create(input) {
448
+ return this.http.post("/lists", input);
449
+ }
450
+ async update(id, input) {
451
+ return this.http.patch(`/lists/${encodeURIComponent(id)}`, input);
452
+ }
453
+ async delete(id) {
454
+ await this.http.delete(`/lists/${encodeURIComponent(id)}`);
455
+ }
456
+ async listContacts(id, query = {}) {
457
+ return this.http.get(
458
+ `/lists/${encodeURIComponent(id)}/contacts?${toQueryString3(query)}`
459
+ );
460
+ }
461
+ async addContacts(id, contactIds) {
462
+ return this.http.post(
463
+ `/lists/${encodeURIComponent(id)}/contacts`,
464
+ { contactIds }
465
+ );
466
+ }
467
+ async removeContact(id, contactId) {
468
+ await this.http.delete(
469
+ `/lists/${encodeURIComponent(id)}/contacts/${encodeURIComponent(contactId)}`
470
+ );
471
+ }
472
+ };
473
+
474
+ // src/sms.ts
475
+ function toQueryString4(query) {
476
+ const params = new URLSearchParams();
477
+ if (query.status) params.set("status", query.status);
478
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
479
+ if (query.cursor) params.set("cursor", query.cursor);
480
+ return params.toString();
481
+ }
482
+ var Sms = class {
483
+ constructor(http) {
484
+ this.http = http;
485
+ }
486
+ http;
487
+ async send(input) {
488
+ return this.http.post("/v1/sms", input);
489
+ }
490
+ async get(id) {
491
+ return this.http.get(
492
+ `/v1/sms/${encodeURIComponent(id)}`
493
+ );
494
+ }
495
+ async list(query = {}) {
496
+ const qs = toQueryString4(query);
497
+ return this.http.get(`/v1/sms${qs ? `?${qs}` : ""}`);
498
+ }
499
+ };
500
+
501
+ // src/webhook.ts
502
+ async function verifyWebhookSignature(payload, signature, secret) {
503
+ const expected = signature.startsWith("sha256=") ? signature.slice(7) : signature;
504
+ const enc = new TextEncoder();
505
+ const key = await globalThis.crypto.subtle.importKey(
506
+ "raw",
507
+ enc.encode(secret),
508
+ { name: "HMAC", hash: "SHA-256" },
509
+ false,
510
+ ["sign"]
511
+ );
512
+ const mac = await globalThis.crypto.subtle.sign(
513
+ "HMAC",
514
+ key,
515
+ enc.encode(payload)
516
+ );
517
+ const computed = Array.from(new Uint8Array(mac)).map((b) => b.toString(16).padStart(2, "0")).join("");
518
+ if (computed.length !== expected.length) return false;
519
+ let diff = 0;
520
+ for (let i = 0; i < computed.length; i++) {
521
+ diff |= computed.charCodeAt(i) ^ expected.charCodeAt(i);
522
+ }
523
+ return diff === 0;
524
+ }
525
+
526
+ // src/types.ts
527
+ var EMAIL_STATUSES = [
528
+ "queued",
529
+ "rendering",
530
+ "submitted_to_postfix",
531
+ "deferred",
532
+ "sent",
533
+ "delivered",
534
+ "bounced",
535
+ "rejected",
536
+ "failed",
537
+ "suppressed",
538
+ "cancelled",
539
+ "scheduled",
540
+ "quarantined"
541
+ ];
542
+ var EMAIL_EVENT_TYPES = [
543
+ "queued",
544
+ "rendering_started",
545
+ "rendering_completed",
546
+ "submitted_to_postfix",
547
+ "deferred",
548
+ "sent",
549
+ "delivered",
550
+ "bounced",
551
+ "rejected",
552
+ "failed",
553
+ "suppressed",
554
+ "cancelled",
555
+ "retry_scheduled",
556
+ "open",
557
+ "click",
558
+ "quarantined"
559
+ ];
560
+ var SMS_STATUSES = [
561
+ "queued",
562
+ "sending",
563
+ "sent",
564
+ "delivered",
565
+ "deferred",
566
+ "failed",
567
+ "suppressed"
568
+ ];
569
+
570
+ // src/index.ts
40
571
  var DevelEmail = class {
41
572
  emails;
573
+ domains;
574
+ templates;
575
+ campaigns;
576
+ contacts;
577
+ lists;
578
+ sms;
42
579
  constructor(config) {
43
- this.emails = new EmailsNamespace(config);
580
+ const http = new HttpClient(config);
581
+ this.emails = new Emails(http);
582
+ this.domains = new Domains(http);
583
+ this.templates = new Templates(http);
584
+ this.campaigns = new Campaigns(http);
585
+ this.contacts = new Contacts(http);
586
+ this.lists = new Lists(http);
587
+ this.sms = new Sms(http);
44
588
  }
45
589
  };
46
590
  export {
47
591
  DevelEmail,
48
- DevelEmailError
592
+ DevelEmailError,
593
+ EMAIL_EVENT_TYPES,
594
+ EMAIL_STATUSES,
595
+ SMS_STATUSES,
596
+ verifyWebhookSignature
49
597
  };
50
598
  //# sourceMappingURL=index.js.map