@pidgeon-ai/sdk 0.1.0 → 0.2.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.cts CHANGED
@@ -72,10 +72,79 @@ interface Thread {
72
72
  subject: string | null;
73
73
  status: 'open' | 'waiting' | 'resolved' | 'human_review';
74
74
  owner_type: 'human' | 'automation' | 'agent';
75
+ /**
76
+ * Which person is answering. Null is the unassigned queue.
77
+ *
78
+ * It composes with `owner_type` rather than replacing it: that says which
79
+ * *kind* of thing is answering, and this says which human. A thread with
80
+ * `status: 'human_review'` and no assignee is a handover nobody has taken.
81
+ */
82
+ assignee_id: string | null;
83
+ assigned_at: string | null;
75
84
  message_count: number;
76
85
  last_message_at: string | null;
77
86
  created_at: string;
78
87
  }
88
+ /**
89
+ * A person in the workspace, for assigning conversations to.
90
+ *
91
+ * No avatar: it is a pointer at an image on somebody else's host, and an
92
+ * integration has no use for one.
93
+ */
94
+ /**
95
+ * One folder of a mailbox, and what is in it.
96
+ *
97
+ * Null rather than zero where the product does not count that folder — zero
98
+ * would be a claim, and the counts query answers what a sidebar needs rather
99
+ * than everything that could be counted.
100
+ */
101
+ interface Mailbox {
102
+ folder: 'inbox' | 'starred' | 'sent' | 'drafts' | 'scheduled' | 'snoozed' | 'archive' | 'spam' | 'trash';
103
+ total: number | null;
104
+ unread: number | null;
105
+ }
106
+ /**
107
+ * A conversation in a folder listing — the newest message in each thread.
108
+ *
109
+ * Deliberately not a `Message`: it has no body, because a list of forty
110
+ * conversations does not want forty bodies. `GET /messages?thread_id=` opens
111
+ * one.
112
+ */
113
+ interface MessageListItem {
114
+ id: string;
115
+ thread_id: string;
116
+ direction: string;
117
+ /** Who the row is about: the sender in inbox, the recipient in sent. */
118
+ from: string;
119
+ from_name: string;
120
+ subject: string | null;
121
+ preview: string | null;
122
+ is_read: boolean;
123
+ is_starred: boolean;
124
+ is_draft: boolean;
125
+ is_spam: boolean;
126
+ has_attachments: boolean;
127
+ attachment_count: number;
128
+ message_count: number;
129
+ assignee_id: string | null;
130
+ timestamp: string;
131
+ }
132
+ /** Every state change the API can apply to a conversation. */
133
+ type MessageActionName = 'read' | 'unread' | 'star' | 'unstar' | 'archive' | 'unarchive' | 'delete' | 'restore' | 'spam' | 'not_spam' | 'delete_forever';
134
+ /** A file uploaded ahead of a send. Expires in a day if never used. */
135
+ interface UploadedAttachment {
136
+ id: string;
137
+ filename: string;
138
+ content_type: string;
139
+ size: number;
140
+ }
141
+ interface Member {
142
+ id: string;
143
+ email: string;
144
+ name: string | null;
145
+ role: 'owner' | 'member';
146
+ joined_at: string;
147
+ }
79
148
  interface Webhook {
80
149
  id: string;
81
150
  url: string;
@@ -97,18 +166,113 @@ interface Event {
97
166
  data: Record<string, unknown>;
98
167
  created_at: string;
99
168
  }
169
+ /**
170
+ * Where mail to an address goes.
171
+ *
172
+ * An address resolves in one of three ways and the first match wins: it *is*
173
+ * an address of its own, a rule points it somewhere, or the domain's
174
+ * catch-all takes it. So a rule never overrides a real address — creating one
175
+ * for an address that already exists is refused rather than silently ignored.
176
+ */
100
177
  interface RoutingRule {
101
178
  id: string;
102
- /** The address or pattern this rule matches. */
179
+ /** The address this rule matches, or `*@domain` for the catch-all. */
103
180
  match: string;
104
- /** `exact` for an address, `catch_all` for the domain's fallback. */
105
- match_type: string;
181
+ /** `address` for one address, `catch_all` for the domain's fallback. */
182
+ match_type: 'address' | 'catch_all';
106
183
  /** `identity` for an inbox, `webhook` for a URL. */
107
- destination_type: string;
184
+ destination_type: 'identity' | 'webhook';
185
+ /** The address or the endpoint URL — what to show somebody. */
108
186
  destination: string;
109
- status: string;
187
+ /** The id of that address or endpoint — what to pass back. */
188
+ destination_id: string | null;
189
+ /** `disabled` keeps the rule and stops it firing. */
190
+ status: 'active' | 'disabled';
110
191
  domain: string;
192
+ domain_id: string;
193
+ created_at: string;
194
+ }
195
+ /**
196
+ * One attempt to hand an event to one endpoint.
197
+ *
198
+ * `status` is ours: `pending` is queued, `sending` is in flight, `delivered`
199
+ * succeeded, `failed` is still being retried and `dead` has stopped being.
200
+ * `response_status` is your server's answer, where there was one — a network
201
+ * failure leaves it null and puts the reason in `error`.
202
+ */
203
+ interface Delivery {
204
+ id: string;
205
+ event_id: string;
206
+ event_type: string;
207
+ status: string;
208
+ attempts: number;
209
+ /** The HTTP status your endpoint answered with, or null if it did not. */
210
+ response_status: number | null;
211
+ error: string | null;
111
212
  created_at: string;
213
+ delivered_at: string | null;
214
+ }
215
+ /**
216
+ * A key, as a record. Never the secret — that is returned once, on create.
217
+ *
218
+ * `identity_ids` is the key's scope: the addresses it reaches, and the
219
+ * ceiling on what a key it mints may reach.
220
+ */
221
+ interface ApiKey {
222
+ id: string;
223
+ name: string;
224
+ /** Enough to recognise it in a list. Not enough to use. */
225
+ prefix: string;
226
+ identity_ids: string[];
227
+ /** The key that minted this one, where a key did. */
228
+ created_by_key_id: string | null;
229
+ last_used_at: string | null;
230
+ revoked_at: string | null;
231
+ created_at: string;
232
+ }
233
+ /**
234
+ * A credential for one mailbox, over IMAP and SMTP.
235
+ *
236
+ * The secret comes back once, on create, as `password` — grouped for typing
237
+ * into a mail client — beside the `username` it must be paired with. A
238
+ * password presented for the wrong address is a failed login.
239
+ */
240
+ interface AppPassword {
241
+ id: string;
242
+ name: string;
243
+ prefix: string;
244
+ last_used_at: string | null;
245
+ revoked_at: string | null;
246
+ created_at: string;
247
+ }
248
+ /**
249
+ * One DNS record a domain needs, and whether it is published yet.
250
+ *
251
+ * `status` is `connected` when it is right, `waiting` when nothing is there,
252
+ * `incorrect` when something else is, and `not_required` for a record this
253
+ * provider does not need. `found` is what we actually read — a record with
254
+ * the right name and the wrong value looks identical to a missing one until
255
+ * you can see it.
256
+ */
257
+ interface DnsRecord {
258
+ /** `ownership`, `mx`, `spf`, `dkim` or `dmarc`. */
259
+ type: string;
260
+ /** `TXT`, `MX` or `CNAME`. */
261
+ record_type: string;
262
+ name: string;
263
+ value: string;
264
+ priority: number | null;
265
+ status: 'connected' | 'waiting' | 'incorrect' | 'not_required';
266
+ found: string | null;
267
+ /** One sentence, written for the person who has to publish it. */
268
+ description: string;
269
+ checked_at: string | null;
270
+ }
271
+ interface Workspace {
272
+ id: string;
273
+ name: string;
274
+ plan: string;
275
+ status: string;
112
276
  }
113
277
  interface Domain {
114
278
  id: string;
@@ -130,7 +294,34 @@ interface Page<T> {
130
294
  data: T[];
131
295
  has_more: boolean;
132
296
  next_cursor: string | null;
297
+ /**
298
+ * Where a delta should start, on the listings that support one.
299
+ *
300
+ * Keep it when you finish the initial load and hand it to
301
+ * `mailboxes.changes`. It is taken before the rows are read, so anything
302
+ * that changed while you were paging comes back in the first delta rather
303
+ * than falling into the gap.
304
+ */
305
+ sync_cursor?: string;
133
306
  }
307
+ /**
308
+ * One thing that happened to a mailbox since a cursor.
309
+ *
310
+ * A change carries the whole conversation, in the same shape a listing row
311
+ * has — so a client holds one type rather than two. A deletion carries the
312
+ * id and nothing else, because there is nothing else left: the message is
313
+ * gone, and a record of what it said would make "delete" mean "delete most
314
+ * of it".
315
+ */
316
+ type MailboxChange = ({
317
+ deleted: false;
318
+ folder: Mailbox['folder'];
319
+ changed_at: string;
320
+ } & MessageListItem) | {
321
+ deleted: true;
322
+ id: string;
323
+ changed_at: string;
324
+ };
134
325
 
135
326
  /**
136
327
  * The Pidgeon client.
@@ -168,7 +359,21 @@ declare class Pidgeon {
168
359
  #private;
169
360
  constructor(options: PidgeonOptions);
170
361
  identities: {
171
- list: () => Promise<Identity[]>;
362
+ /**
363
+ * A page of addresses, alphabetically.
364
+ *
365
+ * `cursor` is the last address on the page before it, not a timestamp —
366
+ * this is the one list that is not ordered by time, because it is read
367
+ * by somebody looking for a particular address.
368
+ *
369
+ * It returned every address and no page before this, which made
370
+ * `const { data } = await pidgeon.identities.list()` — the line in our
371
+ * own quickstart — resolve to `undefined`.
372
+ */
373
+ list: (options?: {
374
+ limit?: number;
375
+ cursor?: string;
376
+ }) => Promise<Page<Identity>>;
172
377
  get: (id: string) => Promise<Identity>;
173
378
  create: (input: {
174
379
  domain_id: string;
@@ -192,6 +397,66 @@ declare class Pidgeon {
192
397
  listInThread: (threadId: string, options?: {
193
398
  limit?: number;
194
399
  }) => Promise<Page<Message>>;
400
+ /**
401
+ * Change one conversation: read, starred, or which folder it is in.
402
+ *
403
+ * Returns an `undo` you can hand to `messages.undo`.
404
+ */
405
+ update: (id: string, input: {
406
+ /**
407
+ * Readable, and **not writable with an API key**. Read state belongs
408
+ * to a person; setting it here is refused rather than silently
409
+ * dropped. Archive the conversation, or track what you have seen
410
+ * with `mailboxes.changes`.
411
+ */
412
+ is_read?: boolean;
413
+ is_starred?: boolean;
414
+ folder?: "inbox" | "archive" | "spam" | "trash";
415
+ }) => Promise<{
416
+ id: string;
417
+ undo: unknown[];
418
+ }>;
419
+ /** One action, up to 200 conversations. */
420
+ act: (messageIds: string[], action: MessageActionName) => Promise<{
421
+ applied: number;
422
+ undo: unknown[];
423
+ }>;
424
+ /**
425
+ * Put an action back.
426
+ *
427
+ * Unusual for an API to have, and the argument is that software makes
428
+ * mistakes at a speed a person does not: an agent that archived the
429
+ * wrong forty conversations should be able to say so.
430
+ */
431
+ undo: (undo: unknown[]) => Promise<{
432
+ restored: number;
433
+ }>;
434
+ /**
435
+ * Search one mailbox, narrowed.
436
+ *
437
+ * Full text in `q`, and six filters beside it. Everything is optional
438
+ * except having *something*: a filter with no words is a legitimate
439
+ * search — "everything unread from Marcus" is a question — and neither
440
+ * words nor filters is not.
441
+ *
442
+ * `in` is the only way to look in Trash, which a plain search skips on
443
+ * purpose. Results say which folder each hit is in.
444
+ */
445
+ search: (identityId: string, options?: {
446
+ q?: string;
447
+ /** One folder. Omit for everywhere except Trash. */
448
+ in?: string;
449
+ /** Substring of the sender's address or display name. */
450
+ from?: string;
451
+ /** Substring of any recipient. */
452
+ to?: string;
453
+ has_attachment?: boolean;
454
+ is_unread?: boolean;
455
+ is_starred?: boolean;
456
+ limit?: number;
457
+ }) => Promise<Page<MessageListItem & {
458
+ folder: string;
459
+ }>>;
195
460
  send: (input: {
196
461
  from: string;
197
462
  to: string | string[];
@@ -199,6 +464,10 @@ declare class Pidgeon {
199
464
  bcc?: string | string[];
200
465
  subject?: string;
201
466
  text?: string;
467
+ /** An HTML body. `text` stays required — see the docs for why. */
468
+ html?: string;
469
+ /** Ids from `attachments.upload`. */
470
+ attachment_ids?: string[];
202
471
  in_reply_to?: string | null;
203
472
  }) => Promise<{
204
473
  id: string;
@@ -214,11 +483,129 @@ declare class Pidgeon {
214
483
  id: string;
215
484
  url: string;
216
485
  }>;
486
+ /**
487
+ * Upload a file, then name its id in `messages.send`.
488
+ *
489
+ * An upload expires in a day: it is a file no message refers to yet, so
490
+ * the window is long enough for a retry and short enough that a crashed
491
+ * client does not leave its files with us.
492
+ */
493
+ upload: (identityId: string, file: File | Blob, filename?: string) => Promise<UploadedAttachment>;
494
+ };
495
+ mailboxes: {
496
+ /** The folders of one address, with their counts. */
497
+ list: (identityId: string) => Promise<Page<Mailbox>>;
498
+ /**
499
+ * One folder, a page at a time.
500
+ *
501
+ * `cursor` is the `next_cursor` of the page before it and is opaque —
502
+ * it carries the sort key and the id together, because several messages
503
+ * can share a millisecond. Do not construct one.
504
+ */
505
+ list_folder: (folder: Mailbox["folder"], identityId: string, options?: {
506
+ limit?: number;
507
+ cursor?: string;
508
+ }) => Promise<Page<MessageListItem>>;
509
+ /**
510
+ * What changed since you last looked.
511
+ *
512
+ * Per mailbox, not per folder: a message moving from the inbox to the
513
+ * archive is one event, and the row says where it went. Arrivals are in
514
+ * it too, so this is the only thing you need to poll.
515
+ *
516
+ * `since` is the `sync_cursor` from a listing, or the `next_cursor` from
517
+ * a previous call to this. It is **not** a page cursor — the two are
518
+ * different orders and passing one for the other is refused rather than
519
+ * guessed at.
520
+ *
521
+ * Throws `resync` when the cursor is older than the window in which we
522
+ * keep the record of what was deleted. List the folders again.
523
+ */
524
+ changes: (identityId: string, since: string, options?: {
525
+ limit?: number;
526
+ }) => Promise<Page<MailboxChange>>;
527
+ /**
528
+ * Empty the trash, permanently.
529
+ *
530
+ * Only the trash. It takes attachments and archived originals with it and
531
+ * there is no undo — `messages.undo` replays column values, and these
532
+ * rows are gone.
533
+ *
534
+ * `messages.act(ids, 'delete_forever')` does the same to a list you name,
535
+ * and caps at 200. This needs no ids.
536
+ */
537
+ empty_trash: (identityId: string) => Promise<{
538
+ folder: string;
539
+ deleted: number;
540
+ }>;
541
+ };
542
+ workspace: {
543
+ /** Which account this key is for, and which plan sets its limits. */
544
+ get: () => Promise<Workspace>;
545
+ /**
546
+ * The people here, so a conversation can be assigned to one of them.
547
+ *
548
+ * Read-only. Inviting somebody is mail to a person and removing somebody
549
+ * hands on their mailboxes; neither is a thing to do without a human
550
+ * deciding it.
551
+ */
552
+ members: () => Promise<Page<Member>>;
553
+ };
554
+ /**
555
+ * A credential per customer, minted by this one.
556
+ *
557
+ * `identity_ids` is required and must be a subset of what this key already
558
+ * reaches — a key cannot delegate reach it does not have. Hand the new key
559
+ * to the tenant it was scoped for, and revoke it when they leave.
560
+ *
561
+ * **Revoking takes the keys it made with it**, transitively. That is what
562
+ * makes rotating a leaked key true: its first useful move would otherwise
563
+ * be to mint a second one and keep it.
564
+ */
565
+ keys: {
566
+ list: () => Promise<ApiKey[]>;
567
+ /** The response carries `token`, once. Store it before you discard it. */
568
+ create: (input: {
569
+ name: string;
570
+ identity_ids: string[];
571
+ }) => Promise<ApiKey & {
572
+ token: string;
573
+ }>;
574
+ /** Revokes it and everything it minted. A key may revoke itself. */
575
+ revoke: (id: string) => Promise<{
576
+ id: string;
577
+ }>;
578
+ };
579
+ /**
580
+ * Working IMAP and SMTP credentials for one address.
581
+ *
582
+ * What lets you hand a customer a mailbox they can open in Apple Mail. The
583
+ * address you may issue one for is an address this key can already send
584
+ * from — the credential reads the inbox and sends as it, so issuing one is
585
+ * handing over exactly that.
586
+ *
587
+ * `username` and `password` are useless apart: a password presented for the
588
+ * wrong address is a failed login, not a successful one.
589
+ */
590
+ appPasswords: {
591
+ list: (identityId: string) => Promise<AppPassword[]>;
592
+ create: (input: {
593
+ identity_id: string;
594
+ name: string;
595
+ }) => Promise<AppPassword & {
596
+ username: string;
597
+ password: string;
598
+ }>;
599
+ revoke: (id: string) => Promise<{
600
+ id: string;
601
+ }>;
217
602
  };
218
603
  threads: {
219
604
  list: (options?: {
220
605
  identity_id?: string;
221
606
  status?: string;
607
+ /** A person's id, or `'unassigned'` for the queue nobody has taken. */
608
+ assignee_id?: string;
222
609
  limit?: number;
223
610
  cursor?: string;
224
611
  }) => Promise<Page<Thread>>;
@@ -226,6 +613,8 @@ declare class Pidgeon {
226
613
  update: (id: string, input: {
227
614
  status?: "open" | "waiting" | "resolved" | "human_review";
228
615
  owner_type?: "human" | "automation" | "agent";
616
+ /** A person's id to hand it to, or `null` to put it back in the queue. */
617
+ assignee_id?: string | null;
229
618
  }) => Promise<Thread>;
230
619
  /** Hand the conversation to a person. The takeover primitive, named. */
231
620
  handOff: (id: string) => Promise<Thread>;
@@ -243,6 +632,25 @@ declare class Pidgeon {
243
632
  delete: (id: string) => Promise<{
244
633
  id: string;
245
634
  }>;
635
+ /**
636
+ * Turn an endpoint off, or back on.
637
+ *
638
+ * We disable one ourselves after enough consecutive failures. Without
639
+ * this you would have to delete and recreate it, which changes the
640
+ * signing secret every receiver is verifying against.
641
+ */
642
+ update: (id: string, input: {
643
+ status: "active" | "disabled";
644
+ }) => Promise<Webhook>;
645
+ /** What happened to each event we tried to hand this endpoint. */
646
+ deliveries: (webhookId: string, options?: {
647
+ limit?: number;
648
+ }) => Promise<Delivery[]>;
649
+ /** Queue one up again. Not idempotent — sending twice is the point. */
650
+ replay: (deliveryId: string) => Promise<{
651
+ id: string;
652
+ status: string;
653
+ }>;
246
654
  };
247
655
  events: {
248
656
  list: (options?: {
@@ -253,9 +661,88 @@ declare class Pidgeon {
253
661
  }) => Promise<Page<Event>>;
254
662
  get: (id: string) => Promise<Event>;
255
663
  };
664
+ /**
665
+ * Domains, and the one step this API cannot perform for you.
666
+ *
667
+ * We cannot write records in somebody else's zone. What we can do is make
668
+ * the human step **delegable**: `create` hands back the records alongside
669
+ * the domain, so you show them to your own customer in your own interface
670
+ * and never send anybody to a Pidgeon screen. Then poll `verify`, or
671
+ * subscribe to `domain.verified` and do not poll at all.
672
+ *
673
+ * `dns_complete` is not a detail: the DKIM records come from what the mail
674
+ * provider issued, so a list returned before provisioning finished looks
675
+ * complete and is missing three CNAMEs. Publishing that gives you a domain
676
+ * that verifies and cannot sign mail.
677
+ */
256
678
  domains: {
257
- /** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
258
679
  list: () => Promise<Domain[]>;
680
+ get: (id: string) => Promise<Domain & {
681
+ dns_records: DnsRecord[];
682
+ dns_complete: boolean;
683
+ }>;
684
+ create: (input: {
685
+ domain: string;
686
+ }) => Promise<Domain & {
687
+ dns_records: DnsRecord[];
688
+ dns_complete: boolean;
689
+ }>;
690
+ /** Idempotent and safe to poll. Calling it on a verified domain re-checks. */
691
+ verify: (id: string) => Promise<{
692
+ verified: boolean;
693
+ mail_configured: boolean;
694
+ message: string;
695
+ dns_records: DnsRecord[];
696
+ dns_complete: boolean;
697
+ }>;
698
+ /** Takes its addresses, their mail, their rules and their webhooks with it. */
699
+ delete: (id: string) => Promise<{
700
+ id: string;
701
+ }>;
702
+ /**
703
+ * The catch-all switch — the one thing about a domain that can change.
704
+ *
705
+ * Catch-all is two gates: this, and a `catch_all` routing rule saying
706
+ * where the mail goes. Turning this on without the rule does nothing, so
707
+ * that turning it off later does not throw away where the mail was going.
708
+ */
709
+ update: (id: string, input: {
710
+ catch_all_enabled: boolean;
711
+ }) => Promise<Domain>;
712
+ };
713
+ /**
714
+ * Where mail to an address goes.
715
+ *
716
+ * The piece that makes per-customer addresses work without a person in the
717
+ * loop: `acme@yourdomain.com` routed into one inbox you already have, or
718
+ * into a webhook with no inbox at all. An address always beats a rule, so a
719
+ * rule never shadows a real address — creating one that would is refused.
720
+ *
721
+ * The destination must be an address you could already send from, because
722
+ * routing an address to a mailbox also lets that mailbox send as it.
723
+ */
724
+ routing: {
725
+ list: (options?: {
726
+ domain_id?: string;
727
+ }) => Promise<RoutingRule[]>;
728
+ create: (input: {
729
+ match_type: "address";
730
+ local_part: string;
731
+ destination_identity_id?: string;
732
+ destination_webhook_id?: string;
733
+ } | {
734
+ match_type: "catch_all";
735
+ domain_id: string;
736
+ destination_identity_id?: string;
737
+ destination_webhook_id?: string;
738
+ }) => Promise<RoutingRule>;
739
+ /** Stop a rule firing without forgetting where it pointed. */
740
+ update: (id: string, input: {
741
+ status: "active" | "disabled";
742
+ }) => Promise<RoutingRule>;
743
+ delete: (id: string) => Promise<{
744
+ id: string;
745
+ }>;
259
746
  };
260
747
  }
261
748
 
@@ -299,4 +786,4 @@ declare function verifyWebhook(input: {
299
786
  now?: Date;
300
787
  }): Promise<WebhookEnvelope>;
301
788
 
302
- export { type Address, type Attachment, type Domain, type Event, type Identity, type Message, type Page, Pidgeon, PidgeonError, type PidgeonOptions, type RoutingRule, SIGNATURE_HEADER, TIMESTAMP_HEADER, type Thread, type Webhook, type WebhookEnvelope, verifyWebhook };
789
+ export { type Address, type ApiKey, type AppPassword, type Attachment, type Delivery, type DnsRecord, type Domain, type Event, type Identity, type Mailbox, type MailboxChange, type Member, type Message, type MessageActionName, type MessageListItem, type Page, Pidgeon, PidgeonError, type PidgeonOptions, type RoutingRule, SIGNATURE_HEADER, TIMESTAMP_HEADER, type Thread, type UploadedAttachment, type Webhook, type WebhookEnvelope, type Workspace, verifyWebhook };