@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.d.ts CHANGED
@@ -1,34 +1,557 @@
1
1
  interface DevelEmailConfig {
2
2
  apiKey: string;
3
3
  baseUrl: string;
4
+ maxRetries?: number;
4
5
  }
6
+ /**
7
+ * Wire-format attachment shape — what actually goes over HTTP (`content` is
8
+ * already base64). Mirrors `apps/api/src/domains/emails/types.ts`'s
9
+ * `AttachmentInput` field-for-field; kept as a distinct name from the
10
+ * ergonomic `AttachmentInput` below so the DTO contract test can assert the
11
+ * wire shape while `emails.send()`/`emails.batch()` callers get
12
+ * base64/Buffer ergonomics. Exported only for `apps/api`'s DTO contract
13
+ * test to import — not part of the ergonomic public surface callers build
14
+ * requests with.
15
+ */
16
+ interface WireAttachmentInput {
17
+ filename: string;
18
+ content: string;
19
+ contentType: string;
20
+ }
21
+ /**
22
+ * Ergonomic attachment input for `emails.send()` / `emails.batch()`.
23
+ * `content` accepts a `Uint8Array` (covers Node `Buffer`) — the SDK
24
+ * base64-encodes it before the request goes out — or an already-base64
25
+ * `string`, matching the wire format directly.
26
+ *
27
+ * Server caps: max 10 attachments per email, 5 MiB per file (decoded),
28
+ * 7 MiB combined decoded size per email.
29
+ */
30
+ interface AttachmentInput {
31
+ filename: string;
32
+ content: string | Uint8Array;
33
+ contentType: string;
34
+ }
35
+ /**
36
+ * Deliberately different from `apps/api/src/domains/emails/types.ts`'s
37
+ * `SendEmailInput` in `attachments` only: this is the ergonomic pre-request
38
+ * shape (`AttachmentInput.content: string | Uint8Array`) while the api's
39
+ * `z.infer`'d type is the post-parse wire shape (`content: string`,
40
+ * already base64). Every other field matches exactly — see
41
+ * `apps/api/src/sdk-dto-contract.test.ts`'s `Omit<..., 'attachments'>`
42
+ * assertion. See `apps/api/src/sdk-dto-contract-discovery.test.ts`'s
43
+ * registry for the enforcement.
44
+ */
5
45
  interface SendEmailInput {
6
46
  from: string;
7
47
  to: string | string[];
8
- subject: string;
48
+ subject?: string;
9
49
  html?: string;
10
50
  text?: string;
11
51
  tags?: string[];
12
52
  metadata?: Record<string, unknown>;
53
+ template?: string;
54
+ variables?: Record<string, string>;
55
+ send_at?: string;
56
+ attachments?: AttachmentInput[];
13
57
  }
58
+ interface SendOptions {
59
+ idempotencyKey?: string;
60
+ }
61
+ /**
62
+ * Mirrors `apps/api/src/domains/emails/types.ts`'s server-side copy of this
63
+ * shape. Not imported from there: this package is published to npm for
64
+ * external consumers and can't depend on the private api app. Keep both
65
+ * copies in sync by hand when the wire shape changes; same for
66
+ * `BatchEmailResultItem` / `BatchEmailResponse` below.
67
+ */
14
68
  interface EmailResponse {
15
69
  id: string;
16
70
  status: string;
71
+ duplicate?: boolean;
17
72
  }
18
- declare class DevelEmailError extends Error {
19
- readonly status: number;
20
- constructor(status: number, message: string);
73
+ interface BatchEmailResultItem {
74
+ index: number;
75
+ id?: string;
76
+ status?: string;
77
+ duplicate?: boolean;
78
+ error?: string;
79
+ }
80
+ interface BatchEmailResponse {
81
+ results: BatchEmailResultItem[];
82
+ created: number;
83
+ errors: number;
84
+ limitReached?: boolean;
85
+ }
86
+ interface Domain {
87
+ id: string;
88
+ organizationId: string;
89
+ name: string;
90
+ verificationStatus: string;
91
+ spfStatus: string | null;
92
+ dkimStatus: string | null;
93
+ dmarcStatus: string | null;
94
+ ptrStatus: string | null;
95
+ mxStatus: string | null;
96
+ returnPathStatus: string | null;
97
+ dmarcPolicy: string | null;
98
+ dkimPublicKey: string;
99
+ dkimSelector: string;
100
+ createdAt: string;
101
+ updatedAt: string;
102
+ }
103
+ interface Template {
104
+ id: string;
105
+ organizationId: string;
106
+ name: string;
107
+ subject: string;
108
+ html: string;
109
+ text?: string | null;
110
+ variables?: string[];
111
+ createdAt: string;
112
+ updatedAt: string;
113
+ }
114
+ interface DomainCreateInput {
115
+ name: string;
116
+ dkimSelector?: string;
117
+ }
118
+ interface TemplateCreateInput {
119
+ name: string;
120
+ subject: string;
121
+ html: string;
122
+ text?: string;
123
+ variables?: string[];
124
+ }
125
+ interface TemplateUpdateInput {
126
+ name?: string;
127
+ subject?: string;
128
+ html?: string;
129
+ text?: string;
130
+ variables?: string[];
131
+ }
132
+ /**
133
+ * Every status an email can be in. `delivered` means the recipient's mail
134
+ * gateway accepted the message with a 2xx response — it does not mean the
135
+ * message reached the inbox; it can still be filtered or quarantined
136
+ * afterward.
137
+ */
138
+ declare const EMAIL_STATUSES: readonly ["queued", "rendering", "submitted_to_postfix", "deferred", "sent", "delivered", "bounced", "rejected", "failed", "suppressed", "cancelled", "scheduled", "quarantined"];
139
+ type EmailStatus = (typeof EMAIL_STATUSES)[number];
140
+ /**
141
+ * Every event type the server currently emits, including as a webhook's
142
+ * `event` field. `WebhookEvent.event` stays typed as `string` (not this
143
+ * union) because the server may add new event types over time — treat an
144
+ * unrecognized value as informational, not an error. See
145
+ * `docs/webhooks.md`: a project's webhook receives every event type below
146
+ * automatically, with no per-type opt-in.
147
+ */
148
+ declare const EMAIL_EVENT_TYPES: readonly ["queued", "rendering_started", "rendering_completed", "submitted_to_postfix", "deferred", "sent", "delivered", "bounced", "rejected", "failed", "suppressed", "cancelled", "retry_scheduled", "open", "click", "quarantined"];
149
+ type EmailEventType = (typeof EMAIL_EVENT_TYPES)[number];
150
+ /**
151
+ * `metadata` shape for `delivered` / `deferred` / `bounced` webhook events
152
+ * and lifecycle-timeline entries — written by the Postfix log follower.
153
+ */
154
+ interface DeliveryEventData {
155
+ queueId: string;
156
+ relay: string;
157
+ response: string;
158
+ timestamp: string;
159
+ }
160
+ /**
161
+ * Mirrors `apps/api/src/domains/webhooks/types.ts`'s `WebhookPayload`
162
+ * (different name, same shape) — see that file for why this isn't a shared
163
+ * import.
164
+ */
165
+ interface WebhookEvent {
166
+ event: string;
167
+ emailId: string | null;
168
+ status: string | null;
169
+ recipient: string | null;
170
+ timestamp: string;
171
+ metadata: Record<string, unknown>;
172
+ }
173
+ type CampaignStatus = 'draft' | 'scheduled' | 'sending' | 'paused' | 'sent' | 'cancelled';
174
+ interface Campaign {
175
+ id: string;
176
+ projectId: string;
177
+ listId: string | null;
178
+ tagFilters: string[] | null;
179
+ tagFilterMode: 'and' | 'or' | null;
180
+ templateId: string | null;
181
+ name: string;
182
+ subject: string;
183
+ fromAddress: string;
184
+ fromName: string | null;
185
+ rawHtml: string | null;
186
+ rawText: string | null;
187
+ status: CampaignStatus;
188
+ scheduledFor: string | null;
189
+ sentAt: string | null;
190
+ totalRecipients: number | null;
191
+ summarizedStats: {
192
+ delivered?: number;
193
+ deliveryUnknown?: number;
194
+ opened?: number;
195
+ clicked?: number;
196
+ bounced?: number;
197
+ unsubscribed?: number;
198
+ } | null;
199
+ createdAt: string;
200
+ updatedAt: string;
201
+ }
202
+ /**
203
+ * Deliberately different from `apps/api/src/domains/campaigns/types.ts`'s
204
+ * `CampaignListQuery` in `limit`/`offset` optionality, not a bug: this type
205
+ * describes what a caller may omit before the request is sent, while the
206
+ * api's `z.infer`'d type describes the post-parse shape after `.default()`
207
+ * has filled those fields in. Both are correct for what they represent — see
208
+ * `apps/api/src/sdk-dto-contract-discovery.test.ts`'s registry for the
209
+ * enforcement.
210
+ */
211
+ interface CampaignListQuery {
212
+ projectId: string;
213
+ status?: CampaignStatus;
214
+ limit?: number;
215
+ offset?: number;
216
+ }
217
+ interface CampaignCreateInput {
218
+ projectId: string;
219
+ listId?: string;
220
+ tagFilters?: string[];
221
+ tagFilterMode?: 'and' | 'or';
222
+ templateId?: string;
223
+ name: string;
224
+ subject: string;
225
+ fromAddress: string;
226
+ fromName?: string;
227
+ rawHtml?: string;
228
+ rawText?: string;
229
+ scheduledFor?: string;
230
+ }
231
+ interface CampaignUpdateInput {
232
+ listId?: string | null;
233
+ tagFilters?: string[] | null;
234
+ tagFilterMode?: 'and' | 'or';
235
+ templateId?: string | null;
236
+ name?: string;
237
+ subject?: string;
238
+ fromAddress?: string;
239
+ fromName?: string;
240
+ rawHtml?: string;
241
+ rawText?: string;
242
+ scheduledFor?: string;
243
+ }
244
+ /**
245
+ * Mirrors `apps/api/src/domains/campaigns/types.ts`'s server-side copy of
246
+ * this shape (and `CampaignProviderStats` below) — see that file for why
247
+ * this isn't a shared import.
248
+ */
249
+ interface CampaignStats {
250
+ campaignId: string;
251
+ totalRecipients: number;
252
+ delivered: number;
253
+ deliveryUnknown: number;
254
+ opened: number;
255
+ clicked: number;
256
+ bounced: number;
257
+ unsubscribed: number;
258
+ openRate: number;
259
+ clickRate: number;
260
+ bounceRate: number;
261
+ links: Array<{
262
+ url: string;
263
+ clicks: number;
264
+ }>;
265
+ }
266
+ interface CampaignProviderStats {
267
+ provider: string;
268
+ opens: number;
269
+ clicks: number;
270
+ total: number;
271
+ }
272
+ interface List {
273
+ id: string;
274
+ projectId: string;
275
+ name: string;
276
+ description: string | null;
277
+ createdAt: string;
278
+ updatedAt: string;
279
+ }
280
+ /**
281
+ * Deliberately different from `apps/api/src/domains/lists/types.ts`'s
282
+ * `ListWithCount` in `createdAt`/`updatedAt` (inherited from `List` above),
283
+ * not a bug: this describes what a client receives after the wire
284
+ * round-trip (`string`), while the api's type is the server-side shape held
285
+ * before JSON serialization (`Date`, via drizzle's `$inferSelect`). See
286
+ * `apps/api/src/sdk-dto-contract-discovery.test.ts`'s registry for the
287
+ * enforcement.
288
+ */
289
+ interface ListWithCount extends List {
290
+ memberCount: number;
291
+ }
292
+ interface ListCreateInput {
293
+ projectId: string;
294
+ name: string;
295
+ description?: string;
296
+ }
297
+ interface ListUpdateInput {
298
+ name?: string;
299
+ description?: string;
300
+ }
301
+ interface ListMembersQuery {
302
+ search?: string;
303
+ limit?: number;
304
+ offset?: number;
305
+ }
306
+ interface ListMembersResponse {
307
+ data: Contact[];
308
+ total: number;
309
+ }
310
+ interface Contact {
311
+ id: string;
312
+ projectId: string;
313
+ email: string;
314
+ firstName: string | null;
315
+ lastName: string | null;
316
+ country: string | null;
317
+ tags: string[];
318
+ unsubscribedAt: string | null;
319
+ createdAt: string;
320
+ updatedAt: string;
321
+ }
322
+ /**
323
+ * Deliberately different from `apps/api/src/domains/contacts/types.ts`'s
324
+ * `ContactListQuery` in `tags`/`tagMode`, not a bug: the api type is
325
+ * `z.infer<typeof ContactListQuerySchema>` — `tags` is post-transform
326
+ * `string[]` and `tagMode` is post-`.default()` required — while this is the
327
+ * pre-request shape callers build, where both stay optional. See
328
+ * `apps/api/src/sdk-dto-contract-discovery.test.ts`'s registry for the
329
+ * enforcement.
330
+ */
331
+ interface ContactListQuery {
332
+ projectId: string;
333
+ search?: string;
334
+ tags?: string[];
335
+ tagMode?: 'and' | 'or';
336
+ limit?: number;
337
+ offset?: number;
21
338
  }
22
- declare class EmailsNamespace {
23
- private readonly config;
339
+ interface ContactListResponse {
340
+ data: Contact[];
341
+ total: number;
342
+ }
343
+ interface ContactCreateInput {
344
+ projectId: string;
345
+ email: string;
346
+ firstName?: string;
347
+ lastName?: string;
348
+ tags?: string[];
349
+ }
350
+ interface ContactUpdateInput {
351
+ unsubscribe: boolean;
352
+ }
353
+ interface ContactBulkContactInput {
354
+ email: string;
355
+ firstName?: string;
356
+ lastName?: string;
357
+ country?: string;
358
+ tags?: string[];
359
+ }
360
+ interface ContactBulkInput {
361
+ projectId: string;
362
+ contacts: ContactBulkContactInput[];
363
+ }
364
+ interface ContactBulkResponse {
365
+ inserted: number;
366
+ updated: number;
367
+ skipped: number;
368
+ }
369
+ /**
370
+ * Deliberately different from `apps/api/src/domains/contacts/types.ts`'s
371
+ * `ContactListItem` in `addedAt`, not a bug: this describes what a client
372
+ * receives after the wire round-trip (`string`), while the api's type is
373
+ * the server-side shape held before JSON serialization (`Date`). See
374
+ * `apps/api/src/sdk-dto-contract-discovery.test.ts`'s registry for the
375
+ * enforcement.
376
+ */
377
+ interface ContactListItem {
378
+ id: string;
379
+ name: string;
380
+ memberCount: number;
381
+ addedAt: string;
382
+ }
383
+ /**
384
+ * Mirrors `apps/api/src/domains/sms/repo.types.ts`'s `smsStatusEnum` values
385
+ * (`packages/db/src/schema/enums.ts`). Kept as a local literal union rather
386
+ * than an import — this package can't depend on `@develemail/db`, same
387
+ * reason `EMAIL_STATUSES` above is hand-copied instead of imported.
388
+ */
389
+ declare const SMS_STATUSES: readonly ["queued", "sending", "sent", "delivered", "deferred", "failed", "suppressed"];
390
+ type SmsStatus = (typeof SMS_STATUSES)[number];
391
+ /**
392
+ * Mirrors `apps/api/src/domains/sms/types.ts`'s `z.infer`'d `SendSmsInput`
393
+ * field-for-field — unlike `SendEmailInput`, there's no ergonomic wire
394
+ * conversion needed here, so this is asserted equal (not
395
+ * `DELIBERATELY_DIFFERENT`) in `apps/api/src/sdk-dto-contract.test.ts`.
396
+ * Provide exactly one of `body` or `templateName` — the server 400s
397
+ * otherwise; that XOR is a runtime `.refine()`, not expressible in the type.
398
+ */
399
+ interface SendSmsInput {
400
+ to: string;
401
+ from?: string;
402
+ body?: string;
403
+ templateName?: string;
404
+ variables?: Record<string, string>;
405
+ tags?: string[];
406
+ metadata?: Record<string, unknown>;
407
+ }
408
+ /**
409
+ * Mirrors `apps/api/src/domains/sms/types.ts`'s server-side copy of this
410
+ * shape field-for-field. Not imported from there: this package is published
411
+ * to npm for external consumers and can't depend on the private api app.
412
+ * Keep both copies in sync by hand when the wire shape changes; same for
413
+ * `SmsListResponse` below.
414
+ */
415
+ interface SmsMessageResponse {
416
+ id: string;
417
+ to: string;
418
+ from: string | null;
419
+ body: string;
420
+ status: string;
421
+ tags: string[];
422
+ metadata: Record<string, unknown>;
423
+ createdAt: string;
424
+ updatedAt: string;
425
+ }
426
+ interface SmsListResponse {
427
+ data: SmsMessageResponse[];
428
+ nextCursor: string | null;
429
+ }
430
+ interface SmsListQuery {
431
+ status?: SmsStatus;
432
+ limit?: number;
433
+ cursor?: string;
434
+ }
435
+
436
+ declare class HttpClient {
437
+ private readonly baseUrl;
438
+ private readonly apiKey;
439
+ private readonly maxRetries;
24
440
  constructor(config: DevelEmailConfig);
25
- send(input: SendEmailInput): Promise<EmailResponse>;
26
- get(id: string): Promise<EmailResponse>;
441
+ get<T>(path: string): Promise<T>;
442
+ post<T>(path: string, body: unknown, extraHeaders?: Record<string, string>): Promise<T>;
443
+ put<T>(path: string, body: unknown, extraHeaders?: Record<string, string>): Promise<T>;
444
+ patch<T>(path: string, body: unknown, extraHeaders?: Record<string, string>): Promise<T>;
445
+ delete<T>(path: string, extraHeaders?: Record<string, string>): Promise<T>;
27
446
  private request;
447
+ private backoff;
28
448
  }
449
+
450
+ declare class Emails {
451
+ private readonly http;
452
+ constructor(http: HttpClient);
453
+ send(input: SendEmailInput, options?: SendOptions): Promise<EmailResponse>;
454
+ batch(emails: SendEmailInput[], options?: SendOptions): Promise<BatchEmailResponse>;
455
+ get(id: string): Promise<EmailResponse>;
456
+ }
457
+
458
+ declare class Domains {
459
+ private readonly http;
460
+ constructor(http: HttpClient);
461
+ list(): Promise<Domain[]>;
462
+ get(id: string): Promise<Domain>;
463
+ create(input: DomainCreateInput): Promise<Domain>;
464
+ delete(id: string): Promise<void>;
465
+ verify(id: string): Promise<Domain>;
466
+ }
467
+
468
+ declare class Templates {
469
+ private readonly http;
470
+ constructor(http: HttpClient);
471
+ list(): Promise<Template[]>;
472
+ get(id: string): Promise<Template>;
473
+ create(input: TemplateCreateInput): Promise<Template>;
474
+ update(id: string, input: TemplateUpdateInput): Promise<Template>;
475
+ delete(id: string): Promise<void>;
476
+ }
477
+
478
+ declare class Campaigns {
479
+ private readonly http;
480
+ constructor(http: HttpClient);
481
+ list(query: CampaignListQuery): Promise<Campaign[]>;
482
+ get(id: string): Promise<Campaign>;
483
+ create(input: CampaignCreateInput): Promise<Campaign>;
484
+ update(id: string, input: CampaignUpdateInput): Promise<Campaign>;
485
+ send(id: string): Promise<Campaign>;
486
+ pause(id: string): Promise<Campaign>;
487
+ resume(id: string): Promise<Campaign>;
488
+ cancel(id: string): Promise<Campaign & {
489
+ note: string;
490
+ }>;
491
+ duplicate(id: string): Promise<Campaign>;
492
+ delete(id: string): Promise<void>;
493
+ stats(id: string): Promise<CampaignStats>;
494
+ providerStats(id: string): Promise<CampaignProviderStats[]>;
495
+ }
496
+
497
+ declare class Contacts {
498
+ private readonly http;
499
+ constructor(http: HttpClient);
500
+ list(query: ContactListQuery): Promise<ContactListResponse>;
501
+ get(id: string): Promise<Contact>;
502
+ create(input: ContactCreateInput): Promise<Contact>;
503
+ update(id: string, input: ContactUpdateInput): Promise<Contact>;
504
+ delete(id: string): Promise<void>;
505
+ bulkCreate(input: ContactBulkInput): Promise<ContactBulkResponse>;
506
+ tags(projectId: string): Promise<string[]>;
507
+ lists(contactId: string): Promise<ContactListItem[]>;
508
+ /** @deprecated Use `lists.addContacts` instead. */
509
+ addToList(contactId: string, listId: string): Promise<void>;
510
+ /** @deprecated Use `lists.removeContact` instead. */
511
+ removeFromList(contactId: string, listId: string): Promise<void>;
512
+ }
513
+
514
+ declare class Lists {
515
+ private readonly http;
516
+ constructor(http: HttpClient);
517
+ list(projectId: string): Promise<ListWithCount[]>;
518
+ get(id: string): Promise<List>;
519
+ create(input: ListCreateInput): Promise<List>;
520
+ update(id: string, input: ListUpdateInput): Promise<List>;
521
+ delete(id: string): Promise<void>;
522
+ listContacts(id: string, query?: ListMembersQuery): Promise<ListMembersResponse>;
523
+ addContacts(id: string, contactIds: string[]): Promise<{
524
+ added: number;
525
+ }>;
526
+ removeContact(id: string, contactId: string): Promise<void>;
527
+ }
528
+
529
+ declare class Sms {
530
+ private readonly http;
531
+ constructor(http: HttpClient);
532
+ send(input: SendSmsInput): Promise<SmsMessageResponse>;
533
+ get(id: string): Promise<SmsMessageResponse>;
534
+ list(query?: SmsListQuery): Promise<SmsListResponse>;
535
+ }
536
+
537
+ declare class DevelEmailError extends Error {
538
+ readonly status: number;
539
+ readonly code?: string | undefined;
540
+ readonly details?: Record<string, unknown> | undefined;
541
+ constructor(status: number, message: string, code?: string | undefined, details?: Record<string, unknown> | undefined);
542
+ }
543
+
544
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
545
+
29
546
  declare class DevelEmail {
30
- readonly emails: EmailsNamespace;
547
+ readonly emails: Emails;
548
+ readonly domains: Domains;
549
+ readonly templates: Templates;
550
+ readonly campaigns: Campaigns;
551
+ readonly contacts: Contacts;
552
+ readonly lists: Lists;
553
+ readonly sms: Sms;
31
554
  constructor(config: DevelEmailConfig);
32
555
  }
33
556
 
34
- export { DevelEmail, type DevelEmailConfig, DevelEmailError, type EmailResponse, type SendEmailInput };
557
+ export { type AttachmentInput, type BatchEmailResponse, type BatchEmailResultItem, type Campaign, type CampaignCreateInput, type CampaignListQuery, type CampaignProviderStats, type CampaignStats, type CampaignStatus, type CampaignUpdateInput, type Contact, type ContactBulkContactInput, type ContactBulkInput, type ContactBulkResponse, type ContactCreateInput, type ContactListItem, type ContactListQuery, type ContactListResponse, type ContactUpdateInput, type DeliveryEventData, DevelEmail, type DevelEmailConfig, DevelEmailError, type Domain, type DomainCreateInput, EMAIL_EVENT_TYPES, EMAIL_STATUSES, type EmailEventType, type EmailResponse, type EmailStatus, type List, type ListCreateInput, type ListMembersQuery, type ListMembersResponse, type ListUpdateInput, type ListWithCount, SMS_STATUSES, type SendEmailInput, type SendOptions, type SendSmsInput, type SmsListQuery, type SmsListResponse, type SmsMessageResponse, type SmsStatus, type Template, type TemplateCreateInput, type TemplateUpdateInput, type WebhookEvent, type WireAttachmentInput, verifyWebhookSignature };