tosend 1.0.1 → 1.1.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/README.md CHANGED
@@ -110,15 +110,46 @@ response.results.forEach((result) => {
110
110
  ```typescript
111
111
  const info = await tosend.getAccountInfo();
112
112
 
113
- console.log(info.account.title);
114
- console.log(info.account.emails_usage_this_month);
115
- console.log(info.account.emails_sent_last_24hrs);
113
+ console.log(info.account.status); // 'active'
114
+ console.log(info.account.credit_balance); // null = no credit limit
115
+ console.log(info.account.limit_per_second);
116
116
 
117
117
  info.domains.forEach((domain) => {
118
118
  console.log(domain.domain_name, domain.verification_status);
119
119
  });
120
120
  ```
121
121
 
122
+ ## Admin API
123
+
124
+ `ToSendAdmin` is a separate client for the [Admin API](https://tosend.com/docs/api/admin-api). It takes an admin key (`tsend_admin_...`) and refuses a sending key at construction. Each call needs the matching [scope](https://tosend.com/docs/api/admin-api#scopes) on the key.
125
+
126
+ ```typescript
127
+ import { ToSendAdmin } from 'tosend';
128
+
129
+ const admin = new ToSendAdmin(process.env.TOSEND_ADMIN_KEY!);
130
+
131
+ // Act on one of your client teams (sends X-Account-Id)
132
+ const client = admin.forAccount(4203);
133
+
134
+ const domain = await client.createDomain('client.example.com');
135
+ const detail = await client.getDomain(domain.id);
136
+ console.log(detail.dns_records.dkim); // show these records to your client
137
+
138
+ // Needs api_keys:write and emails:send
139
+ const key = await client.createSendingKey('Client: Acme Corp');
140
+ console.log(key.api_key); // returned once, store it now
141
+
142
+ // Needs emails:send
143
+ await client.send({
144
+ from: { email: 'hello@client.example.com' },
145
+ to: [{ email: 'user@example.com' }],
146
+ subject: 'Hi',
147
+ html: '<p>Hello</p>',
148
+ });
149
+ ```
150
+
151
+ Also available: `getInfo` (checks the key and lists its scopes), `listDomains`, `deleteDomain`, `listEmails`, `getEmail`, `listSuppressions`, `deleteSuppression`, `listWebhooks`, `createWebhook`, `updateWebhook`, `deleteWebhook`, `listSendingKeys`, `deleteSendingKey`, `listTeams`, `createTeam`, `updateTeam` and `batch`. List methods take `page` and `per_page` and return `{ data, total, page, per_page }`.
152
+
122
153
  ## Configuration
123
154
 
124
155
  ### Simple (API key only)
package/dist/index.d.mts CHANGED
@@ -45,23 +45,36 @@ interface BatchEmailResult {
45
45
  status: 'success' | 'error' | 'spam';
46
46
  message_id?: string;
47
47
  message?: string;
48
- errors?: Record<string, Record<string, string>>;
48
+ errors?: ValidationErrors;
49
49
  }
50
50
  interface BatchEmailResponse {
51
51
  results: BatchEmailResult[];
52
52
  }
53
53
  interface Domain {
54
+ id: number;
54
55
  domain_name: string;
55
- verification_status: 'pending' | 'verified' | 'failed';
56
- created_at: string;
56
+ verification_status: string;
57
+ /** 1 for the account's default domain, else 0 */
58
+ is_default: number;
59
+ total_sent: number;
60
+ total_bounced: number;
61
+ total_complained: number;
57
62
  }
58
63
  interface AccountInfo {
59
64
  account: {
60
- title: string;
61
- plan_type: string;
65
+ id: number;
62
66
  status: string;
63
- emails_usage_this_month: number;
64
- emails_sent_last_24hrs: number;
67
+ plan_type: string;
68
+ /** null means the account does not consume credits */
69
+ credit_balance: number | null;
70
+ monthly_email_limit: number;
71
+ limit_per_second: number;
72
+ ses_region: string;
73
+ /** Fraction between 0 and 1, not a percentage */
74
+ bounce_rate: number;
75
+ /** Fraction between 0 and 1, not a percentage */
76
+ complaint_rate: number;
77
+ ses_status: string;
65
78
  };
66
79
  domains: Domain[];
67
80
  }
@@ -69,8 +82,25 @@ interface ToSendErrorResponse {
69
82
  status_code: number;
70
83
  error_type: ErrorType;
71
84
  message: string;
72
- errors?: Record<string, Record<string, string>>;
85
+ /**
86
+ * Field-level validation detail.
87
+ *
88
+ * The sending API returns a nested map (`{subject: {required: "..."}}`);
89
+ * the Worker's ApiError returns a flat one (`{name: "required"}`). Both
90
+ * shapes reach this type, so consumers must not assume either — see
91
+ * ToSendError.errors.
92
+ */
93
+ errors?: ValidationErrors;
73
94
  }
95
+ /**
96
+ * Either shape the API may return for `errors`.
97
+ *
98
+ * ToSendError declared the nested form only, so `err.errors.name` was typed
99
+ * as an object while the Worker actually sends the string `'required'`. That
100
+ * compiled because the response was cast, and would have surfaced as an
101
+ * undefined property at runtime.
102
+ */
103
+ type ValidationErrors = Record<string, string | Record<string, string>>;
74
104
  type ErrorType = 'bad_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'validation_error' | 'rate_limit_exceeded' | 'internal_error';
75
105
  interface ToSendConfig {
76
106
  /** Your ToSend API key */
@@ -83,9 +113,10 @@ interface ToSendConfig {
83
113
  declare class ToSendError extends Error {
84
114
  readonly code: number;
85
115
  readonly errorType: ErrorType;
86
- readonly errors: Record<string, Record<string, string>>;
116
+ /** See ValidationErrors — the value may be a string or a nested map. */
117
+ readonly errors: ValidationErrors;
87
118
  private static readonly ERROR_TYPE_MAP;
88
- constructor(message: string, code?: number, errors?: Record<string, Record<string, string>>, errorType?: ErrorType);
119
+ constructor(message: string, code?: number, errors?: ValidationErrors, errorType?: ErrorType);
89
120
  get isValidationError(): boolean;
90
121
  get isAuthenticationError(): boolean;
91
122
  get isRateLimitError(): boolean;
@@ -154,5 +185,318 @@ declare class ToSend {
154
185
  private isValidEmail;
155
186
  private request;
156
187
  }
188
+ interface AdminDomain {
189
+ /** Domain identifier used across the Admin API */
190
+ id: string;
191
+ domain_name: string;
192
+ verification_status: 'draft' | 'pending' | 'verified' | 'failed';
193
+ is_default: boolean;
194
+ ses_region: string | null;
195
+ total_sent: number;
196
+ total_bounced: number;
197
+ total_complained: number;
198
+ updated_at: string;
199
+ }
200
+ interface DnsRecord {
201
+ type: 'TXT' | 'MX';
202
+ name: string;
203
+ content: string;
204
+ TTL: string;
205
+ priority: string;
206
+ /**
207
+ * NOT returned by the Admin API.
208
+ *
209
+ * The domain-detail endpoint does not resolve DNS on your behalf (that
210
+ * would make a GET fire four lookups and a write), so there is no
211
+ * per-record status. Poll `verification_status` on the domain instead —
212
+ * it is the result of the automatic 10-minute check.
213
+ *
214
+ * @deprecated Always undefined. Kept so existing code still compiles.
215
+ */
216
+ status?: string;
217
+ }
218
+ interface CreatedDomain {
219
+ /** Same identifier the list and detail endpoints use */
220
+ id: string;
221
+ domain_name: string;
222
+ verification_status: string;
223
+ created_at: string;
224
+ }
225
+ interface AdminDomainDetail {
226
+ id: string;
227
+ domain_name: string;
228
+ verification_status: string;
229
+ dmarc_policy: string | null;
230
+ created_at: string;
231
+ dns_records: {
232
+ dkim: DnsRecord;
233
+ spf: DnsRecord[];
234
+ };
235
+ /** Whether the automatic 10-minute DNS check still covers this domain */
236
+ auto_verification: {
237
+ active: boolean;
238
+ reason: string;
239
+ };
240
+ }
241
+ interface EmailLog {
242
+ /** Same value POST /v2/emails returned as message_id */
243
+ id: string;
244
+ status: string;
245
+ subject: string | null;
246
+ from: string | null;
247
+ from_name: string | null;
248
+ to: Array<{
249
+ email: string;
250
+ name?: string;
251
+ }>;
252
+ reply_to: string | null;
253
+ recipients_count: number;
254
+ error: string | null;
255
+ has_attachments: boolean;
256
+ sent_at: string | null;
257
+ created_at: string;
258
+ }
259
+ interface Paginated<T> {
260
+ data: T[];
261
+ total: number;
262
+ page: number;
263
+ per_page: number;
264
+ }
265
+ interface ListLogsParams {
266
+ page?: number;
267
+ /** 1-100, default 25 */
268
+ per_page?: number;
269
+ status?: string;
270
+ /** Domain name or domain id */
271
+ domain?: string;
272
+ from?: string;
273
+ to?: string;
274
+ search?: string;
275
+ }
276
+ interface Suppression {
277
+ email: string;
278
+ bounce_sub_type: string | null;
279
+ bounce_reason: string | null;
280
+ hard_bounced_at: string | null;
281
+ last_complaint_at: string | null;
282
+ complaint_count: number;
283
+ created_at: string;
284
+ updated_at: string;
285
+ }
286
+ /** The only events toSend emits. Delivery, open and click do not exist. */
287
+ type WebhookEvent = 'bounced' | 'complaint';
288
+ interface Webhook {
289
+ id: number;
290
+ title: string | null;
291
+ url: string;
292
+ events: WebhookEvent[];
293
+ status: string;
294
+ include_message: boolean;
295
+ created_at: string;
296
+ updated_at: string;
297
+ }
298
+ interface SendingKey {
299
+ id: number;
300
+ title: string | null;
301
+ /** Masked. The full key is returned only by createSendingKey(). */
302
+ key: string;
303
+ status: string;
304
+ domain: string | null;
305
+ created_at: string;
306
+ }
307
+ interface CreatedSendingKey {
308
+ id: number;
309
+ name: string;
310
+ /** Shown once. Store it now — it cannot be retrieved later. */
311
+ api_key: string;
312
+ created_at: string;
313
+ }
314
+ /** GET /v2/info with an admin key */
315
+ interface AdminInfo {
316
+ account: {
317
+ id: number;
318
+ status: string;
319
+ plan_type: string;
320
+ /** The pooled balance for the whole account, never a child team's own */
321
+ credit_balance: number | null;
322
+ credits_shared: boolean;
323
+ limit_per_second: number;
324
+ ses_region: string;
325
+ bounce_rate: number;
326
+ complaint_rate: number;
327
+ };
328
+ key: {
329
+ id: number;
330
+ title: string;
331
+ type: 'admin';
332
+ /** The team this request acted on */
333
+ acting_on: number;
334
+ /** The team that owns the key */
335
+ owned_by: number;
336
+ scopes: string[];
337
+ };
338
+ /** Present only when the key has `domains:read` */
339
+ domains?: AdminDomain[];
340
+ }
341
+ interface SuppressionReactivation {
342
+ deleted: boolean;
343
+ message: string;
344
+ used_today: number;
345
+ remaining_today: number;
346
+ daily_limit: number;
347
+ }
348
+ interface TeamList {
349
+ account: {
350
+ id: number;
351
+ name: string | null;
352
+ status: string;
353
+ credit_balance: number | null;
354
+ };
355
+ teams: Array<{
356
+ id: number;
357
+ name: string | null;
358
+ status: string;
359
+ }>;
360
+ /** Credits pool at the parent; child teams have no separate balance */
361
+ credits_shared: boolean;
362
+ }
363
+ interface AdminConfig {
364
+ /** Your admin API key (tsend_admin_...) */
365
+ apiKey: string;
366
+ /** Act on a child team by default. Overridable per call. */
367
+ accountId?: number;
368
+ baseUrl?: string;
369
+ timeout?: number;
370
+ }
371
+ /**
372
+ * Account-level API client.
373
+ *
374
+ * Deliberately a separate class from `ToSend`: admin keys and sending keys are
375
+ * different credentials and are not interchangeable. A sending key cannot
376
+ * manage the account, and an admin key can only send when it holds the
377
+ * `emails:send` scope. Keeping them on separate clients means the mistake is
378
+ * caught at the type level rather than by a 401 in production.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * const admin = new ToSendAdmin(process.env.TOSEND_ADMIN_KEY!);
383
+ *
384
+ * const domain = await admin.createDomain('client.example.com');
385
+ * const detail = await admin.getDomain(domain.id);
386
+ * console.log(detail.dns_records.dkim);
387
+ * // No verify call needed — toSend checks DNS every 10 minutes.
388
+ * ```
389
+ */
390
+ declare class ToSendAdmin {
391
+ private readonly apiKey;
392
+ private readonly baseUrl;
393
+ private readonly timeout;
394
+ private readonly accountId?;
395
+ constructor(config: string | AdminConfig);
396
+ /**
397
+ * The team this key acts on, the key itself and its scopes. Needs no scope,
398
+ * so it is the way to check a key works.
399
+ */
400
+ getInfo(): Promise<AdminInfo>;
401
+ /** Return a client scoped to one of your child teams. */
402
+ forAccount(accountId: number): ToSendAdmin;
403
+ /**
404
+ * Send an email as the targeted team (the key's own, or one set with
405
+ * forAccount()). The key must hold the `emails:send` scope.
406
+ */
407
+ send(params: SendEmailParams): Promise<SendEmailResponse>;
408
+ /** Send up to 100 emails in one request; same scope and targeting as send(). */
409
+ batch(emails: SendEmailParams[]): Promise<BatchEmailResponse>;
410
+ listDomains(params?: {
411
+ page?: number;
412
+ per_page?: number;
413
+ }): Promise<Paginated<AdminDomain>>;
414
+ /**
415
+ * Add a sending domain.
416
+ *
417
+ * The domain starts in `draft`. There is NO verify call to make: toSend
418
+ * checks public DNS every 10 minutes and provisions the domain as soon as
419
+ * the records resolve.
420
+ *
421
+ * A duplicate throws a 409. Treat that as "already exists, go read it" —
422
+ * domain names are globally unique, so a retry after a timeout hits this
423
+ * for a domain your first call created successfully.
424
+ */
425
+ createDomain(name: string): Promise<CreatedDomain>;
426
+ /** Domain detail plus the DNS records your customer must publish. */
427
+ getDomain(id: string): Promise<AdminDomainDetail>;
428
+ /**
429
+ * Remove a domain.
430
+ *
431
+ * Does NOT remove anything from your customer's DNS zone. Records left
432
+ * published can allow the domain to be re-verified elsewhere, so clean them
433
+ * up in your own workflow.
434
+ */
435
+ deleteDomain(id: string): Promise<void>;
436
+ listEmails(params?: ListLogsParams): Promise<Paginated<EmailLog>>;
437
+ /** Look up one message by the id POST /v2/emails returned. */
438
+ getEmail(messageId: string): Promise<EmailLog>;
439
+ listSuppressions(params?: {
440
+ page?: number;
441
+ per_page?: number;
442
+ search?: string;
443
+ status?: string;
444
+ }): Promise<Paginated<Suppression>>;
445
+ /** Limited to 500 per day per account, shared with the dashboard. */
446
+ deleteSuppression(email: string): Promise<SuppressionReactivation>;
447
+ listWebhooks(params?: {
448
+ page?: number;
449
+ per_page?: number;
450
+ }): Promise<Paginated<Webhook>>;
451
+ /**
452
+ * The signing `secret` is generated by toSend. The API returns it only in
453
+ * this response (the dashboard can show it later), so store it now.
454
+ */
455
+ createWebhook(params: {
456
+ url: string;
457
+ events: WebhookEvent[];
458
+ title?: string;
459
+ include_message?: boolean;
460
+ }): Promise<Webhook & {
461
+ secret: string;
462
+ }>;
463
+ updateWebhook(id: number, params: Partial<{
464
+ url: string;
465
+ events: WebhookEvent[];
466
+ title: string;
467
+ status: 'active' | 'inactive';
468
+ include_message: boolean;
469
+ }>): Promise<Webhook>;
470
+ deleteWebhook(id: number): Promise<void>;
471
+ listSendingKeys(params?: {
472
+ page?: number;
473
+ per_page?: number;
474
+ }): Promise<Paginated<SendingKey>>;
475
+ /**
476
+ * Mint a sending key for this team — how you give each client their own key.
477
+ *
478
+ * The raw key is returned once and never again. Pass `domain` (a domain id)
479
+ * to restrict it to one domain; omit it for all domains on the team.
480
+ * Needs the `api_keys:write` and `emails:send` scopes.
481
+ */
482
+ createSendingKey(name: string, domain?: string): Promise<CreatedSendingKey>;
483
+ deleteSendingKey(id: number): Promise<void>;
484
+ listTeams(): Promise<TeamList>;
485
+ /** Only the parent account can create teams. */
486
+ createTeam(name: string): Promise<{
487
+ id: number;
488
+ name: string;
489
+ status: string;
490
+ }>;
491
+ updateTeam(id: number, params: {
492
+ name?: string;
493
+ status?: 'active' | 'suspended';
494
+ }): Promise<{
495
+ id: number;
496
+ name: string;
497
+ status: string;
498
+ }>;
499
+ private request;
500
+ }
157
501
 
158
- export { type AccountInfo, type Address, type Attachment, type BatchEmailResponse, type BatchEmailResult, type Domain, type ErrorType, type SendEmailParams, type SendEmailResponse, ToSend, type ToSendConfig, ToSendError, type ToSendErrorResponse, ToSend as default };
502
+ export { type AccountInfo, type Address, type AdminConfig, type AdminDomain, type AdminDomainDetail, type AdminInfo, type Attachment, type BatchEmailResponse, type BatchEmailResult, type CreatedDomain, type CreatedSendingKey, type DnsRecord, type Domain, type EmailLog, type ErrorType, type ListLogsParams, type Paginated, type SendEmailParams, type SendEmailResponse, type SendingKey, type Suppression, type SuppressionReactivation, type TeamList, ToSend, ToSendAdmin, type ToSendConfig, ToSendError, type ToSendErrorResponse, type ValidationErrors, type Webhook, type WebhookEvent, ToSend as default };