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