@develemail/sdk 0.1.1 → 0.4.1

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