@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/CHANGELOG.md CHANGED
@@ -9,6 +9,57 @@ method signatures, the error shape — not to the API behind it.
9
9
 
10
10
  ## Unreleased
11
11
 
12
+ ## 0.2.0 — the mailbox, and the credentials to reach it
13
+
14
+ A minor rather than a patch, because one signature changed. In `0.x` semver
15
+ that is what a minor is for, and the alternative — leaving the old shape
16
+ beside the new one — would have shipped two ways to search that disagree
17
+ about what the second argument is.
18
+
19
+ **Breaking**
20
+
21
+ - `messages.search(identityId, q, options)` is now
22
+ `messages.search(identityId, options)`, with `q` inside the options beside
23
+ six filters. Search takes more than words now, and a positional query in
24
+ front of an options bag would have been the only parameter in the client
25
+ that works that way.
26
+ - `messages.update` no longer accepts `is_read` from an API key — the field
27
+ is still in the type, is still readable, and the API refuses to set it.
28
+ Read state belongs to a person and a key is not one; it used to return
29
+ `200` and change nothing, which is worse. Archive the conversation, or
30
+ track what you have seen with `mailboxes.changes`.
31
+
32
+ **New resources**
33
+
34
+ - `mailboxes` — `list`, `list_folder`, `changes`, `empty_trash`. The folders
35
+ with their counts, one folder by keyset cursor, what changed since a marker,
36
+ and emptying the trash.
37
+ - `routing` — `list`, `create`, `update`, `delete`. Per-customer addresses,
38
+ delivered into an inbox you already have or into a webhook with no inbox
39
+ at all.
40
+ - `keys` — `list`, `create`, `revoke`. A key can mint a narrower key, and
41
+ revoking one revokes everything it minted.
42
+ - `appPasswords` — `list`, `create`, `revoke`. Working IMAP and SMTP
43
+ credentials for one address, which is how you hand a customer a mailbox
44
+ they can open in a mail client.
45
+ - `workspace.get` — which account this key is for, and the plan that sets its
46
+ limits.
47
+
48
+ **New on what was already there**
49
+
50
+ - `domains` gained `create`, `get`, `verify` and `delete`. `create` returns
51
+ the records to publish alongside the domain, because showing them to your
52
+ own customer is the one thing every caller does next.
53
+ - `webhooks` gained `update` (enable and disable), `deliveries` and `replay`.
54
+ - `attachments.upload`, and `html` and `attachment_ids` on `messages.send`.
55
+ - `messages.act`, `messages.undo`, `threads.update`.
56
+
57
+ **New types** — `ApiKey`, `AppPassword`, `Delivery`, `DnsRecord`, `Mailbox`,
58
+ `MailboxChange`, `Member`, `MessageActionName`, `MessageListItem`,
59
+ `UploadedAttachment`, `Workspace`. `RoutingRule` gained `domain_id` and
60
+ `destination_id`, and its `match_type` documentation said `exact`, which was
61
+ never a value the API emits.
62
+
12
63
  ## 0.1.0
13
64
 
14
65
  First release.
package/dist/index.cjs CHANGED
@@ -82,7 +82,18 @@ var Pidgeon = class {
82
82
  }
83
83
  // --- identities ---------------------------------------------------------
84
84
  identities = {
85
- list: () => this.#request("GET", "/identities"),
85
+ /**
86
+ * A page of addresses, alphabetically.
87
+ *
88
+ * `cursor` is the last address on the page before it, not a timestamp —
89
+ * this is the one list that is not ordered by time, because it is read
90
+ * by somebody looking for a particular address.
91
+ *
92
+ * It returned every address and no page before this, which made
93
+ * `const { data } = await pidgeon.identities.list()` — the line in our
94
+ * own quickstart — resolve to `undefined`.
95
+ */
96
+ list: (options = {}) => this.#requestPage("GET", "/identities", options),
86
97
  get: (id) => this.#request("GET", `/identities/${id}`),
87
98
  create: (input) => this.#request("POST", "/identities", { body: input }),
88
99
  update: (id, input) => this.#request("PATCH", `/identities/${id}`, { body: input }),
@@ -95,6 +106,39 @@ var Pidgeon = class {
95
106
  thread_id: threadId,
96
107
  limit: options.limit
97
108
  }),
109
+ /**
110
+ * Change one conversation: read, starred, or which folder it is in.
111
+ *
112
+ * Returns an `undo` you can hand to `messages.undo`.
113
+ */
114
+ update: (id, input) => this.#request("PATCH", `/messages/${id}`, { body: input }),
115
+ /** One action, up to 200 conversations. */
116
+ act: (messageIds, action) => this.#request("POST", "/messages/actions", {
117
+ body: { message_ids: messageIds, action }
118
+ }),
119
+ /**
120
+ * Put an action back.
121
+ *
122
+ * Unusual for an API to have, and the argument is that software makes
123
+ * mistakes at a speed a person does not: an agent that archived the
124
+ * wrong forty conversations should be able to say so.
125
+ */
126
+ undo: (undo) => this.#request("POST", "/messages/undo", { body: { undo } }),
127
+ /**
128
+ * Search one mailbox, narrowed.
129
+ *
130
+ * Full text in `q`, and six filters beside it. Everything is optional
131
+ * except having *something*: a filter with no words is a legitimate
132
+ * search — "everything unread from Marcus" is a question — and neither
133
+ * words nor filters is not.
134
+ *
135
+ * `in` is the only way to look in Trash, which a plain search skips on
136
+ * purpose. Results say which folder each hit is in.
137
+ */
138
+ search: (identityId, options = {}) => this.#requestPage("GET", "/messages/search", {
139
+ identity_id: identityId,
140
+ ...options
141
+ }),
98
142
  send: (input) => this.#request(
99
143
  "POST",
100
144
  "/messages",
@@ -103,7 +147,137 @@ var Pidgeon = class {
103
147
  };
104
148
  attachments = {
105
149
  /** A short-lived signed URL. Fetch it when you need it, don't store it. */
106
- downloadUrl: (id) => this.#request("GET", `/attachments/${id}`)
150
+ downloadUrl: (id) => this.#request("GET", `/attachments/${id}`),
151
+ /**
152
+ * Upload a file, then name its id in `messages.send`.
153
+ *
154
+ * An upload expires in a day: it is a file no message refers to yet, so
155
+ * the window is long enough for a retry and short enough that a crashed
156
+ * client does not leave its files with us.
157
+ */
158
+ upload: async (identityId, file, filename) => {
159
+ const form = new FormData();
160
+ form.set("identity_id", identityId);
161
+ form.set("file", file, filename);
162
+ const response = await this.#fetch(new URL(`${this.#baseUrl}/attachments`), {
163
+ method: "POST",
164
+ headers: { Authorization: `Bearer ${this.#apiKey}` },
165
+ body: form
166
+ });
167
+ const payload = await response.json().catch(() => null);
168
+ if (!response.ok || payload?.error) {
169
+ throw new PidgeonError({
170
+ code: payload?.error?.code ?? "internal",
171
+ message: payload?.error?.message ?? `Request failed with ${response.status}.`,
172
+ status: response.status,
173
+ field: payload?.error?.field,
174
+ requestId: response.headers.get("x-request-id") ?? void 0
175
+ });
176
+ }
177
+ return payload.data;
178
+ }
179
+ };
180
+ // --- mailboxes ----------------------------------------------------------
181
+ mailboxes = {
182
+ /** The folders of one address, with their counts. */
183
+ list: (identityId) => this.#requestPage("GET", "/mailboxes", { identity_id: identityId }),
184
+ /**
185
+ * One folder, a page at a time.
186
+ *
187
+ * `cursor` is the `next_cursor` of the page before it and is opaque —
188
+ * it carries the sort key and the id together, because several messages
189
+ * can share a millisecond. Do not construct one.
190
+ */
191
+ list_folder: (folder, identityId, options = {}) => this.#requestPage("GET", `/mailboxes/${folder}`, {
192
+ identity_id: identityId,
193
+ ...options
194
+ }),
195
+ /**
196
+ * What changed since you last looked.
197
+ *
198
+ * Per mailbox, not per folder: a message moving from the inbox to the
199
+ * archive is one event, and the row says where it went. Arrivals are in
200
+ * it too, so this is the only thing you need to poll.
201
+ *
202
+ * `since` is the `sync_cursor` from a listing, or the `next_cursor` from
203
+ * a previous call to this. It is **not** a page cursor — the two are
204
+ * different orders and passing one for the other is refused rather than
205
+ * guessed at.
206
+ *
207
+ * Throws `resync` when the cursor is older than the window in which we
208
+ * keep the record of what was deleted. List the folders again.
209
+ */
210
+ changes: (identityId, since, options = {}) => this.#requestPage("GET", "/mailboxes/changes", {
211
+ identity_id: identityId,
212
+ since,
213
+ ...options
214
+ }),
215
+ /**
216
+ * Empty the trash, permanently.
217
+ *
218
+ * Only the trash. It takes attachments and archived originals with it and
219
+ * there is no undo — `messages.undo` replays column values, and these
220
+ * rows are gone.
221
+ *
222
+ * `messages.act(ids, 'delete_forever')` does the same to a list you name,
223
+ * and caps at 200. This needs no ids.
224
+ */
225
+ empty_trash: (identityId) => this.#request(
226
+ "DELETE",
227
+ `/mailboxes/trash?identity_id=${identityId}`
228
+ )
229
+ };
230
+ // --- workspace ----------------------------------------------------------
231
+ workspace = {
232
+ /** Which account this key is for, and which plan sets its limits. */
233
+ get: () => this.#request("GET", "/workspace"),
234
+ /**
235
+ * The people here, so a conversation can be assigned to one of them.
236
+ *
237
+ * Read-only. Inviting somebody is mail to a person and removing somebody
238
+ * hands on their mailboxes; neither is a thing to do without a human
239
+ * deciding it.
240
+ */
241
+ members: () => this.#requestPage("GET", "/workspace/members", {})
242
+ };
243
+ // --- credentials --------------------------------------------------------
244
+ /**
245
+ * A credential per customer, minted by this one.
246
+ *
247
+ * `identity_ids` is required and must be a subset of what this key already
248
+ * reaches — a key cannot delegate reach it does not have. Hand the new key
249
+ * to the tenant it was scoped for, and revoke it when they leave.
250
+ *
251
+ * **Revoking takes the keys it made with it**, transitively. That is what
252
+ * makes rotating a leaked key true: its first useful move would otherwise
253
+ * be to mint a second one and keep it.
254
+ */
255
+ keys = {
256
+ list: () => this.#request("GET", "/api-keys"),
257
+ /** The response carries `token`, once. Store it before you discard it. */
258
+ create: (input) => this.#request("POST", "/api-keys", { body: input }),
259
+ /** Revokes it and everything it minted. A key may revoke itself. */
260
+ revoke: (id) => this.#request("DELETE", `/api-keys/${id}`)
261
+ };
262
+ /**
263
+ * Working IMAP and SMTP credentials for one address.
264
+ *
265
+ * What lets you hand a customer a mailbox they can open in Apple Mail. The
266
+ * address you may issue one for is an address this key can already send
267
+ * from — the credential reads the inbox and sends as it, so issuing one is
268
+ * handing over exactly that.
269
+ *
270
+ * `username` and `password` are useless apart: a password presented for the
271
+ * wrong address is a failed login, not a successful one.
272
+ */
273
+ appPasswords = {
274
+ list: (identityId) => this.#request("GET", `/app-passwords?identity_id=${identityId}`),
275
+ create: (input) => this.#request(
276
+ "POST",
277
+ "/app-passwords",
278
+ { body: input }
279
+ ),
280
+ revoke: (id) => this.#request("DELETE", `/app-passwords/${id}`)
107
281
  };
108
282
  // --- threads ------------------------------------------------------------
109
283
  threads = {
@@ -120,16 +294,87 @@ var Pidgeon = class {
120
294
  list: () => this.#request("GET", "/webhooks"),
121
295
  /** The response carries `secret`, once. Store it before you discard it. */
122
296
  create: (input) => this.#request("POST", "/webhooks", { body: input }),
123
- delete: (id) => this.#request("DELETE", `/webhooks/${id}`)
297
+ delete: (id) => this.#request("DELETE", `/webhooks/${id}`),
298
+ /**
299
+ * Turn an endpoint off, or back on.
300
+ *
301
+ * We disable one ourselves after enough consecutive failures. Without
302
+ * this you would have to delete and recreate it, which changes the
303
+ * signing secret every receiver is verifying against.
304
+ */
305
+ update: (id, input) => this.#request("PATCH", `/webhooks/${id}`, { body: input }),
306
+ /** What happened to each event we tried to hand this endpoint. */
307
+ deliveries: (webhookId, options = {}) => this.#request(
308
+ "GET",
309
+ `/deliveries?webhook_id=${webhookId}` + (options.limit === void 0 ? "" : `&limit=${options.limit}`)
310
+ ),
311
+ /** Queue one up again. Not idempotent — sending twice is the point. */
312
+ replay: (deliveryId) => this.#request("POST", `/deliveries/${deliveryId}/replay`)
124
313
  };
125
314
  // --- events -------------------------------------------------------------
126
315
  events = {
127
316
  list: (options = {}) => this.#requestPage("GET", "/events", options),
128
317
  get: (id) => this.#request("GET", `/events/${id}`)
129
318
  };
319
+ /**
320
+ * Domains, and the one step this API cannot perform for you.
321
+ *
322
+ * We cannot write records in somebody else's zone. What we can do is make
323
+ * the human step **delegable**: `create` hands back the records alongside
324
+ * the domain, so you show them to your own customer in your own interface
325
+ * and never send anybody to a Pidgeon screen. Then poll `verify`, or
326
+ * subscribe to `domain.verified` and do not poll at all.
327
+ *
328
+ * `dns_complete` is not a detail: the DKIM records come from what the mail
329
+ * provider issued, so a list returned before provisioning finished looks
330
+ * complete and is missing three CNAMEs. Publishing that gives you a domain
331
+ * that verifies and cannot sign mail.
332
+ */
130
333
  domains = {
131
- /** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
132
- list: () => this.#request("GET", "/domains")
334
+ list: () => this.#request("GET", "/domains"),
335
+ get: (id) => this.#request(
336
+ "GET",
337
+ `/domains/${id}`
338
+ ),
339
+ create: (input) => this.#request(
340
+ "POST",
341
+ "/domains",
342
+ { body: input }
343
+ ),
344
+ /** Idempotent and safe to poll. Calling it on a verified domain re-checks. */
345
+ verify: (id) => this.#request("POST", `/domains/${id}/verify`),
346
+ /** Takes its addresses, their mail, their rules and their webhooks with it. */
347
+ delete: (id) => this.#request("DELETE", `/domains/${id}`),
348
+ /**
349
+ * The catch-all switch — the one thing about a domain that can change.
350
+ *
351
+ * Catch-all is two gates: this, and a `catch_all` routing rule saying
352
+ * where the mail goes. Turning this on without the rule does nothing, so
353
+ * that turning it off later does not throw away where the mail was going.
354
+ */
355
+ update: (id, input) => this.#request("PATCH", `/domains/${id}`, { body: input })
356
+ };
357
+ // --- routing ------------------------------------------------------------
358
+ /**
359
+ * Where mail to an address goes.
360
+ *
361
+ * The piece that makes per-customer addresses work without a person in the
362
+ * loop: `acme@yourdomain.com` routed into one inbox you already have, or
363
+ * into a webhook with no inbox at all. An address always beats a rule, so a
364
+ * rule never shadows a real address — creating one that would is refused.
365
+ *
366
+ * The destination must be an address you could already send from, because
367
+ * routing an address to a mailbox also lets that mailbox send as it.
368
+ */
369
+ routing = {
370
+ list: (options = {}) => this.#request(
371
+ "GET",
372
+ options.domain_id ? `/routing-rules?domain_id=${options.domain_id}` : "/routing-rules"
373
+ ),
374
+ create: (input) => this.#request("POST", "/routing-rules", { body: input }),
375
+ /** Stop a rule firing without forgetting where it pointed. */
376
+ update: (id, input) => this.#request("PATCH", `/routing-rules/${id}`, { body: input }),
377
+ delete: (id) => this.#request("DELETE", `/routing-rules/${id}`)
133
378
  };
134
379
  async #requestPage(method, path, query) {
135
380
  const url = new URL(`${this.#baseUrl}${path}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["/**\n * `@pidgeon-ai/sdk` — a thin typed client for the Pidgeon email API.\n *\n * Three things and no more: typed `fetch` wrappers, one error class, and a\n * webhook verifier.\n *\n * import { Pidgeon, verifyWebhook } from '@pidgeon-ai/sdk';\n *\n * The module split follows what can be wrong on its own. `wire.ts` is the\n * shape of every response and is the single definition the application\n * checks its own DTOs against. `client.ts` is transport. `webhooks.ts` is\n * the only part with a security contract.\n */\n\nexport { Pidgeon, PidgeonError, type PidgeonOptions } from './client';\n\nexport {\n verifyWebhook,\n SIGNATURE_HEADER,\n TIMESTAMP_HEADER,\n type WebhookEnvelope,\n} from './webhooks';\n\nexport type {\n Address,\n Attachment,\n Domain,\n Event,\n Identity,\n Message,\n Page,\n RoutingRule,\n Thread,\n Webhook,\n} from './wire';\n","import type { Domain, Event, Identity, Message, Page, Thread, Webhook } from './wire';\n\n/**\n * The Pidgeon client.\n *\n * Deliberately thin: typed wrappers over `fetch`, one error class, and\n * nothing else. No retry policy, no queue, no caching, no ambient state —\n * those are decisions that belong to the application, and an SDK that makes\n * them for you is an SDK you eventually have to work around.\n *\n * Zero dependencies. The only runtime requirements are `fetch` and\n * `crypto.subtle`, both standard in Node 18+, Bun, Deno and every edge\n * runtime.\n */\n\nexport interface PidgeonOptions {\n apiKey: string;\n /** Defaults to the hosted API. Point this at a local dev server to test. */\n baseUrl?: string;\n /** Injected in tests. Defaults to the global. */\n fetch?: typeof globalThis.fetch;\n}\n\nexport class PidgeonError extends Error {\n readonly code: string;\n readonly status: number;\n readonly field?: string;\n readonly requestId?: string;\n\n constructor(input: {\n code: string;\n message: string;\n status: number;\n field?: string;\n requestId?: string;\n }) {\n super(input.message);\n this.name = 'PidgeonError';\n this.code = input.code;\n this.status = input.status;\n this.field = input.field;\n this.requestId = input.requestId;\n }\n}\n\nconst DEFAULT_BASE_URL = 'https://pidgeon.ai/api/v1';\n\nexport class Pidgeon {\n #apiKey: string;\n #baseUrl: string;\n #fetch: typeof globalThis.fetch;\n\n constructor(options: PidgeonOptions) {\n if (!options.apiKey) throw new Error('An API key is required.');\n\n this.#apiKey = options.apiKey;\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n async #request<T>(\n method: string,\n path: string,\n options: { query?: Record<string, string | number | undefined>; body?: unknown } = {},\n ): Promise<T> {\n const url = new URL(`${this.#baseUrl}${path}`);\n\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n\n const response = await this.#fetch(url, {\n method,\n headers: {\n Authorization: `Bearer ${this.#apiKey}`,\n ...(options.body ? { 'Content-Type': 'application/json' } : {}),\n },\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n\n const requestId = response.headers.get('x-request-id') ?? undefined;\n const payload = (await response.json().catch(() => null)) as {\n data?: unknown;\n error?: { code: string; message: string; field?: string };\n } | null;\n\n if (!response.ok || payload?.error) {\n throw new PidgeonError({\n code: payload?.error?.code ?? 'internal',\n message: payload?.error?.message ?? `Request failed with ${response.status}.`,\n status: response.status,\n field: payload?.error?.field,\n requestId,\n });\n }\n\n return payload?.data as T;\n }\n\n // --- identities ---------------------------------------------------------\n\n identities = {\n list: () => this.#request<Identity[]>('GET', '/identities'),\n get: (id: string) => this.#request<Identity>('GET', `/identities/${id}`),\n create: (input: {\n domain_id: string;\n local_part: string;\n display_name?: string | null;\n type?: 'human' | 'team' | 'application' | 'workflow' | 'agent';\n mode?: 'human' | 'automated' | 'hybrid';\n }) => this.#request<Identity>('POST', '/identities', { body: input }),\n update: (\n id: string,\n input: {\n display_name?: string | null;\n mode?: 'human' | 'automated' | 'hybrid';\n capabilities?: Record<string, boolean>;\n status?: 'active' | 'suspended' | 'archived';\n },\n ) => this.#request<Identity>('PATCH', `/identities/${id}`, { body: input }),\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/identities/${id}`),\n };\n\n // --- messages -----------------------------------------------------------\n\n messages = {\n get: (id: string) => this.#request<Message>('GET', `/messages/${id}`),\n listInThread: (threadId: string, options: { limit?: number } = {}) =>\n this.#requestPage<Message>('GET', '/messages', {\n thread_id: threadId,\n limit: options.limit,\n }),\n send: (input: {\n from: string;\n to: string | string[];\n cc?: string | string[];\n bcc?: string | string[];\n subject?: string;\n text?: string;\n in_reply_to?: string | null;\n }) =>\n this.#request<{ id: string; thread_id: string; rejected: { address: string }[] }>(\n 'POST',\n '/messages',\n { body: input },\n ),\n };\n\n attachments = {\n /** A short-lived signed URL. Fetch it when you need it, don't store it. */\n downloadUrl: (id: string) =>\n this.#request<{ id: string; url: string }>('GET', `/attachments/${id}`),\n };\n\n // --- threads ------------------------------------------------------------\n\n threads = {\n list: (\n options: { identity_id?: string; status?: string; limit?: number; cursor?: string } = {},\n ) => this.#requestPage<Thread>('GET', '/threads', options),\n get: (id: string) => this.#request<Thread>('GET', `/threads/${id}`),\n update: (\n id: string,\n input: {\n status?: 'open' | 'waiting' | 'resolved' | 'human_review';\n owner_type?: 'human' | 'automation' | 'agent';\n },\n ) => this.#request<Thread>('PATCH', `/threads/${id}`, { body: input }),\n /** Hand the conversation to a person. The takeover primitive, named. */\n handOff: (id: string) =>\n this.#request<Thread>('PATCH', `/threads/${id}`, {\n body: { owner_type: 'human', status: 'human_review' },\n }),\n };\n\n // --- webhooks -----------------------------------------------------------\n\n webhooks = {\n list: () => this.#request<Webhook[]>('GET', '/webhooks'),\n /** The response carries `secret`, once. Store it before you discard it. */\n create: (input: { url: string; identity_id?: string | null; event_types?: string[] }) =>\n this.#request<Webhook & { secret: string }>('POST', '/webhooks', { body: input }),\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/webhooks/${id}`),\n };\n\n // --- events -------------------------------------------------------------\n\n events = {\n list: (\n options: { type?: string; identity_id?: string; limit?: number; cursor?: string } = {},\n ) => this.#requestPage<Event>('GET', '/events', options),\n get: (id: string) => this.#request<Event>('GET', `/events/${id}`),\n };\n\n domains = {\n /** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */\n list: () => this.#request<Domain[]>('GET', '/domains'),\n };\n\n async #requestPage<T>(\n method: string,\n path: string,\n query: Record<string, string | number | undefined>,\n ): Promise<Page<T>> {\n const url = new URL(`${this.#baseUrl}${path}`);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n\n const response = await this.#fetch(url, {\n method,\n headers: { Authorization: `Bearer ${this.#apiKey}` },\n });\n\n const payload = (await response.json().catch(() => null)) as\n (Page<T> & { error?: { code: string; message: string; field?: string } }) | null;\n\n if (!response.ok || payload?.error) {\n throw new PidgeonError({\n code: payload?.error?.code ?? 'internal',\n message: payload?.error?.message ?? `Request failed with ${response.status}.`,\n status: response.status,\n field: payload?.error?.field,\n requestId: response.headers.get('x-request-id') ?? undefined,\n });\n }\n\n return {\n data: payload?.data ?? [],\n has_more: payload?.has_more ?? false,\n next_cursor: payload?.next_cursor ?? null,\n };\n }\n}\n","/**\n * Verifying a delivery.\n *\n * The only part of this package with a security contract: it is what a\n * customer runs to decide whether a request claiming a message arrived\n * actually came from us. Everything else here is a typed `fetch`.\n *\n * Two properties carry that contract, and both are tested against payloads\n * signed by the server's own implementation — see\n * `tests/unit/sdk-webhook-verifier.test.ts`. Testing this file against\n * itself would prove only that it agrees with itself, which is exactly the\n * bug two implementations of one scheme produce.\n */\n\nexport const SIGNATURE_HEADER = 'x-pidgeon-signature';\nexport const TIMESTAMP_HEADER = 'x-pidgeon-timestamp';\n\nexport interface WebhookEnvelope {\n id: string;\n type: string;\n created_at: string;\n data: Record<string, unknown>;\n}\n\n/**\n * Verifies a delivery and returns its payload.\n *\n * Throws rather than returning false, because the one thing this must not\n * allow is a caller who forgets to check the boolean. Uses `crypto.subtle`,\n * so it works unchanged in Node, Bun, Deno, Workers and Vercel functions.\n *\n * `rawBody` must be the exact bytes we sent. Re-serialising parsed JSON\n * changes key order and whitespace, and the signature will not match — read\n * the body as text before you parse it.\n */\nexport async function verifyWebhook(input: {\n rawBody: string;\n headers: Headers | Record<string, string | undefined>;\n secret: string;\n toleranceSeconds?: number;\n now?: Date;\n}): Promise<WebhookEnvelope> {\n const get = (name: string): string | undefined =>\n input.headers instanceof Headers\n ? (input.headers.get(name) ?? undefined)\n : (input.headers[name] ?? input.headers[name.toLowerCase()]);\n\n const timestamp = get(TIMESTAMP_HEADER);\n const signature = get(SIGNATURE_HEADER);\n\n if (!timestamp || !signature) throw new Error('This request is not signed.');\n\n const seconds = Number(timestamp);\n if (!Number.isFinite(seconds)) throw new Error('The signature timestamp is not a number.');\n\n const now = Math.floor((input.now ?? new Date()).getTime() / 1000);\n const tolerance = input.toleranceSeconds ?? 300;\n\n if (Math.abs(now - seconds) > tolerance) {\n throw new Error('This request is too old to trust. It may be a replay.');\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(input.secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const mac = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(`${seconds}.${input.rawBody}`),\n );\n\n const expected = `v1=${[...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`;\n\n if (!constantTimeEqual(expected, signature)) {\n throw new Error('The signature does not match.');\n }\n\n return JSON.parse(input.rawBody) as WebhookEnvelope;\n}\n\n/** Length-independent compare. Not `===`, which returns on the first byte. */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n\n let difference = 0;\n for (let index = 0; index < a.length; index += 1) {\n difference |= a.charCodeAt(index) ^ b.charCodeAt(index);\n }\n\n return difference === 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuBO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,OAMT;AACD,UAAM,MAAM,OAAO;AACnB,SAAK,OAAO;AACZ,SAAK,OAAO,MAAM;AAClB,SAAK,SAAS,MAAM;AACpB,SAAK,QAAQ,MAAM;AACnB,SAAK,YAAY,MAAM;AAAA,EACzB;AACF;AAEA,IAAM,mBAAmB;AAElB,IAAM,UAAN,MAAc;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAyB;AACnC,QAAI,CAAC,QAAQ,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAE9D,SAAK,UAAU,QAAQ;AACvB,SAAK,YAAY,QAAQ,WAAW,kBAAkB,QAAQ,OAAO,EAAE;AACvE,SAAK,SAAS,QAAQ,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,SACJ,QACA,MACA,UAAmF,CAAC,GACxE;AACZ,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,QAAQ,GAAG,IAAI,EAAE;AAE7C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,GAAG;AAC9D,UAAI,UAAU,OAAW,KAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAClE;AAEA,UAAM,WAAW,MAAM,KAAK,OAAO,KAAK;AAAA,MACtC;AAAA,MACA,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,OAAO;AAAA,QACrC,GAAI,QAAQ,OAAO,EAAE,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,MAC/D;AAAA,MACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;AAAA,IACtD,CAAC;AAED,UAAM,YAAY,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC1D,UAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AAKvD,QAAI,CAAC,SAAS,MAAM,SAAS,OAAO;AAClC,YAAM,IAAI,aAAa;AAAA,QACrB,MAAM,SAAS,OAAO,QAAQ;AAAA,QAC9B,SAAS,SAAS,OAAO,WAAW,uBAAuB,SAAS,MAAM;AAAA,QAC1E,QAAQ,SAAS;AAAA,QACjB,OAAO,SAAS,OAAO;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAIA,aAAa;AAAA,IACX,MAAM,MAAM,KAAK,SAAqB,OAAO,aAAa;AAAA,IAC1D,KAAK,CAAC,OAAe,KAAK,SAAmB,OAAO,eAAe,EAAE,EAAE;AAAA,IACvE,QAAQ,CAAC,UAMH,KAAK,SAAmB,QAAQ,eAAe,EAAE,MAAM,MAAM,CAAC;AAAA,IACpE,QAAQ,CACN,IACA,UAMG,KAAK,SAAmB,SAAS,eAAe,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAAA,IAC1E,QAAQ,CAAC,OAAe,KAAK,SAAyB,UAAU,eAAe,EAAE,EAAE;AAAA,EACrF;AAAA;AAAA,EAIA,WAAW;AAAA,IACT,KAAK,CAAC,OAAe,KAAK,SAAkB,OAAO,aAAa,EAAE,EAAE;AAAA,IACpE,cAAc,CAAC,UAAkB,UAA8B,CAAC,MAC9D,KAAK,aAAsB,OAAO,aAAa;AAAA,MAC7C,WAAW;AAAA,MACX,OAAO,QAAQ;AAAA,IACjB,CAAC;AAAA,IACH,MAAM,CAAC,UASL,KAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,EAAE,MAAM,MAAM;AAAA,IAChB;AAAA,EACJ;AAAA,EAEA,cAAc;AAAA;AAAA,IAEZ,aAAa,CAAC,OACZ,KAAK,SAAsC,OAAO,gBAAgB,EAAE,EAAE;AAAA,EAC1E;AAAA;AAAA,EAIA,UAAU;AAAA,IACR,MAAM,CACJ,UAAsF,CAAC,MACpF,KAAK,aAAqB,OAAO,YAAY,OAAO;AAAA,IACzD,KAAK,CAAC,OAAe,KAAK,SAAiB,OAAO,YAAY,EAAE,EAAE;AAAA,IAClE,QAAQ,CACN,IACA,UAIG,KAAK,SAAiB,SAAS,YAAY,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAAA;AAAA,IAErE,SAAS,CAAC,OACR,KAAK,SAAiB,SAAS,YAAY,EAAE,IAAI;AAAA,MAC/C,MAAM,EAAE,YAAY,SAAS,QAAQ,eAAe;AAAA,IACtD,CAAC;AAAA,EACL;AAAA;AAAA,EAIA,WAAW;AAAA,IACT,MAAM,MAAM,KAAK,SAAoB,OAAO,WAAW;AAAA;AAAA,IAEvD,QAAQ,CAAC,UACP,KAAK,SAAuC,QAAQ,aAAa,EAAE,MAAM,MAAM,CAAC;AAAA,IAClF,QAAQ,CAAC,OAAe,KAAK,SAAyB,UAAU,aAAa,EAAE,EAAE;AAAA,EACnF;AAAA;AAAA,EAIA,SAAS;AAAA,IACP,MAAM,CACJ,UAAoF,CAAC,MAClF,KAAK,aAAoB,OAAO,WAAW,OAAO;AAAA,IACvD,KAAK,CAAC,OAAe,KAAK,SAAgB,OAAO,WAAW,EAAE,EAAE;AAAA,EAClE;AAAA,EAEA,UAAU;AAAA;AAAA,IAER,MAAM,MAAM,KAAK,SAAmB,OAAO,UAAU;AAAA,EACvD;AAAA,EAEA,MAAM,aACJ,QACA,MACA,OACkB;AAClB,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,QAAQ,GAAG,IAAI,EAAE;AAC7C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,UAAI,UAAU,OAAW,KAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAClE;AAEA,UAAM,WAAW,MAAM,KAAK,OAAO,KAAK;AAAA,MACtC;AAAA,MACA,SAAS,EAAE,eAAe,UAAU,KAAK,OAAO,GAAG;AAAA,IACrD,CAAC;AAED,UAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AAGvD,QAAI,CAAC,SAAS,MAAM,SAAS,OAAO;AAClC,YAAM,IAAI,aAAa;AAAA,QACrB,MAAM,SAAS,OAAO,QAAQ;AAAA,QAC9B,SAAS,SAAS,OAAO,WAAW,uBAAuB,SAAS,MAAM;AAAA,QAC1E,QAAQ,SAAS;AAAA,QACjB,OAAO,SAAS,OAAO;AAAA,QACvB,WAAW,SAAS,QAAQ,IAAI,cAAc,KAAK;AAAA,MACrD,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,MACL,MAAM,SAAS,QAAQ,CAAC;AAAA,MACxB,UAAU,SAAS,YAAY;AAAA,MAC/B,aAAa,SAAS,eAAe;AAAA,IACvC;AAAA,EACF;AACF;;;AC3NO,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AAoBhC,eAAsB,cAAc,OAMP;AAC3B,QAAM,MAAM,CAAC,SACX,MAAM,mBAAmB,UACpB,MAAM,QAAQ,IAAI,IAAI,KAAK,SAC3B,MAAM,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK,YAAY,CAAC;AAE9D,QAAM,YAAY,IAAI,gBAAgB;AACtC,QAAM,YAAY,IAAI,gBAAgB;AAEtC,MAAI,CAAC,aAAa,CAAC,UAAW,OAAM,IAAI,MAAM,6BAA6B;AAE3E,QAAM,UAAU,OAAO,SAAS;AAChC,MAAI,CAAC,OAAO,SAAS,OAAO,EAAG,OAAM,IAAI,MAAM,0CAA0C;AAEzF,QAAM,MAAM,KAAK,OAAO,MAAM,OAAO,oBAAI,KAAK,GAAG,QAAQ,IAAI,GAAI;AACjE,QAAM,YAAY,MAAM,oBAAoB;AAE5C,MAAI,KAAK,IAAI,MAAM,OAAO,IAAI,WAAW;AACvC,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,QAAM,MAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA,IAAI,YAAY,EAAE,OAAO,MAAM,MAAM;AAAA,IACrC,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,MAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA;AAAA,IACA,IAAI,YAAY,EAAE,OAAO,GAAG,OAAO,IAAI,MAAM,OAAO,EAAE;AAAA,EACxD;AAEA,QAAM,WAAW,MAAM,CAAC,GAAG,IAAI,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AAE1G,MAAI,CAAC,kBAAkB,UAAU,SAAS,GAAG;AAC3C,UAAM,IAAI,MAAM,+BAA+B;AAAA,EACjD;AAEA,SAAO,KAAK,MAAM,MAAM,OAAO;AACjC;AAGA,SAAS,kBAAkB,GAAW,GAAoB;AACxD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAElC,MAAI,aAAa;AACjB,WAAS,QAAQ,GAAG,QAAQ,EAAE,QAAQ,SAAS,GAAG;AAChD,kBAAc,EAAE,WAAW,KAAK,IAAI,EAAE,WAAW,KAAK;AAAA,EACxD;AAEA,SAAO,eAAe;AACxB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["/**\n * `@pidgeon-ai/sdk` — a thin typed client for the Pidgeon email API.\n *\n * Three things and no more: typed `fetch` wrappers, one error class, and a\n * webhook verifier.\n *\n * import { Pidgeon, verifyWebhook } from '@pidgeon-ai/sdk';\n *\n * The module split follows what can be wrong on its own. `wire.ts` is the\n * shape of every response and is the single definition the application\n * checks its own DTOs against. `client.ts` is transport. `webhooks.ts` is\n * the only part with a security contract.\n */\n\nexport { Pidgeon, PidgeonError, type PidgeonOptions } from './client';\n\nexport {\n verifyWebhook,\n SIGNATURE_HEADER,\n TIMESTAMP_HEADER,\n type WebhookEnvelope,\n} from './webhooks';\n\nexport type {\n Address,\n Attachment,\n Domain,\n Event,\n Identity,\n Mailbox,\n MailboxChange,\n Member,\n Message,\n MessageActionName,\n MessageListItem,\n Page,\n ApiKey,\n AppPassword,\n Delivery,\n DnsRecord,\n RoutingRule,\n Workspace,\n Thread,\n UploadedAttachment,\n Webhook,\n} from './wire';\n","import type {\n ApiKey,\n AppPassword,\n Delivery,\n DnsRecord,\n Domain,\n Event,\n Identity,\n Mailbox,\n MailboxChange,\n Member,\n Message,\n MessageActionName,\n MessageListItem,\n Page,\n RoutingRule,\n UploadedAttachment,\n Thread,\n Webhook,\n Workspace,\n} from './wire';\n\n/**\n * The Pidgeon client.\n *\n * Deliberately thin: typed wrappers over `fetch`, one error class, and\n * nothing else. No retry policy, no queue, no caching, no ambient state —\n * those are decisions that belong to the application, and an SDK that makes\n * them for you is an SDK you eventually have to work around.\n *\n * Zero dependencies. The only runtime requirements are `fetch` and\n * `crypto.subtle`, both standard in Node 18+, Bun, Deno and every edge\n * runtime.\n */\n\nexport interface PidgeonOptions {\n apiKey: string;\n /** Defaults to the hosted API. Point this at a local dev server to test. */\n baseUrl?: string;\n /** Injected in tests. Defaults to the global. */\n fetch?: typeof globalThis.fetch;\n}\n\nexport class PidgeonError extends Error {\n readonly code: string;\n readonly status: number;\n readonly field?: string;\n readonly requestId?: string;\n\n constructor(input: {\n code: string;\n message: string;\n status: number;\n field?: string;\n requestId?: string;\n }) {\n super(input.message);\n this.name = 'PidgeonError';\n this.code = input.code;\n this.status = input.status;\n this.field = input.field;\n this.requestId = input.requestId;\n }\n}\n\nconst DEFAULT_BASE_URL = 'https://pidgeon.ai/api/v1';\n\nexport class Pidgeon {\n #apiKey: string;\n #baseUrl: string;\n #fetch: typeof globalThis.fetch;\n\n constructor(options: PidgeonOptions) {\n if (!options.apiKey) throw new Error('An API key is required.');\n\n this.#apiKey = options.apiKey;\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n async #request<T>(\n method: string,\n path: string,\n options: { query?: Record<string, string | number | undefined>; body?: unknown } = {},\n ): Promise<T> {\n const url = new URL(`${this.#baseUrl}${path}`);\n\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n\n const response = await this.#fetch(url, {\n method,\n headers: {\n Authorization: `Bearer ${this.#apiKey}`,\n ...(options.body ? { 'Content-Type': 'application/json' } : {}),\n },\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n\n const requestId = response.headers.get('x-request-id') ?? undefined;\n const payload = (await response.json().catch(() => null)) as {\n data?: unknown;\n error?: { code: string; message: string; field?: string };\n } | null;\n\n if (!response.ok || payload?.error) {\n throw new PidgeonError({\n code: payload?.error?.code ?? 'internal',\n message: payload?.error?.message ?? `Request failed with ${response.status}.`,\n status: response.status,\n field: payload?.error?.field,\n requestId,\n });\n }\n\n return payload?.data as T;\n }\n\n // --- identities ---------------------------------------------------------\n\n identities = {\n /**\n * A page of addresses, alphabetically.\n *\n * `cursor` is the last address on the page before it, not a timestamp —\n * this is the one list that is not ordered by time, because it is read\n * by somebody looking for a particular address.\n *\n * It returned every address and no page before this, which made\n * `const { data } = await pidgeon.identities.list()` — the line in our\n * own quickstart — resolve to `undefined`.\n */\n list: (options: { limit?: number; cursor?: string } = {}) =>\n this.#requestPage<Identity>('GET', '/identities', options),\n get: (id: string) => this.#request<Identity>('GET', `/identities/${id}`),\n create: (input: {\n domain_id: string;\n local_part: string;\n display_name?: string | null;\n type?: 'human' | 'team' | 'application' | 'workflow' | 'agent';\n mode?: 'human' | 'automated' | 'hybrid';\n }) => this.#request<Identity>('POST', '/identities', { body: input }),\n update: (\n id: string,\n input: {\n display_name?: string | null;\n mode?: 'human' | 'automated' | 'hybrid';\n capabilities?: Record<string, boolean>;\n status?: 'active' | 'suspended' | 'archived';\n },\n ) => this.#request<Identity>('PATCH', `/identities/${id}`, { body: input }),\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/identities/${id}`),\n };\n\n // --- messages -----------------------------------------------------------\n\n messages = {\n get: (id: string) => this.#request<Message>('GET', `/messages/${id}`),\n listInThread: (threadId: string, options: { limit?: number } = {}) =>\n this.#requestPage<Message>('GET', '/messages', {\n thread_id: threadId,\n limit: options.limit,\n }),\n /**\n * Change one conversation: read, starred, or which folder it is in.\n *\n * Returns an `undo` you can hand to `messages.undo`.\n */\n update: (\n id: string,\n input: {\n /**\n * Readable, and **not writable with an API key**. Read state belongs\n * to a person; setting it here is refused rather than silently\n * dropped. Archive the conversation, or track what you have seen\n * with `mailboxes.changes`.\n */\n is_read?: boolean;\n is_starred?: boolean;\n folder?: 'inbox' | 'archive' | 'spam' | 'trash';\n },\n ) =>\n this.#request<{ id: string; undo: unknown[] }>('PATCH', `/messages/${id}`, { body: input }),\n\n /** One action, up to 200 conversations. */\n act: (messageIds: string[], action: MessageActionName) =>\n this.#request<{ applied: number; undo: unknown[] }>('POST', '/messages/actions', {\n body: { message_ids: messageIds, action },\n }),\n\n /**\n * Put an action back.\n *\n * Unusual for an API to have, and the argument is that software makes\n * mistakes at a speed a person does not: an agent that archived the\n * wrong forty conversations should be able to say so.\n */\n undo: (undo: unknown[]) =>\n this.#request<{ restored: number }>('POST', '/messages/undo', { body: { undo } }),\n\n /**\n * Search one mailbox, narrowed.\n *\n * Full text in `q`, and six filters beside it. Everything is optional\n * except having *something*: a filter with no words is a legitimate\n * search — \"everything unread from Marcus\" is a question — and neither\n * words nor filters is not.\n *\n * `in` is the only way to look in Trash, which a plain search skips on\n * purpose. Results say which folder each hit is in.\n */\n search: (\n identityId: string,\n options: {\n q?: string;\n /** One folder. Omit for everywhere except Trash. */\n in?: string;\n /** Substring of the sender's address or display name. */\n from?: string;\n /** Substring of any recipient. */\n to?: string;\n has_attachment?: boolean;\n is_unread?: boolean;\n is_starred?: boolean;\n limit?: number;\n } = {},\n ) =>\n this.#requestPage<MessageListItem & { folder: string }>('GET', '/messages/search', {\n identity_id: identityId,\n ...options,\n }),\n\n send: (input: {\n from: string;\n to: string | string[];\n cc?: string | string[];\n bcc?: string | string[];\n subject?: string;\n text?: string;\n /** An HTML body. `text` stays required — see the docs for why. */\n html?: string;\n /** Ids from `attachments.upload`. */\n attachment_ids?: string[];\n in_reply_to?: string | null;\n }) =>\n this.#request<{ id: string; thread_id: string; rejected: { address: string }[] }>(\n 'POST',\n '/messages',\n { body: input },\n ),\n };\n\n attachments = {\n /** A short-lived signed URL. Fetch it when you need it, don't store it. */\n downloadUrl: (id: string) =>\n this.#request<{ id: string; url: string }>('GET', `/attachments/${id}`),\n\n /**\n * Upload a file, then name its id in `messages.send`.\n *\n * An upload expires in a day: it is a file no message refers to yet, so\n * the window is long enough for a retry and short enough that a crashed\n * client does not leave its files with us.\n */\n upload: async (identityId: string, file: File | Blob, filename?: string) => {\n const form = new FormData();\n form.set('identity_id', identityId);\n form.set('file', file, filename);\n\n const response = await this.#fetch(new URL(`${this.#baseUrl}/attachments`), {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.#apiKey}` },\n body: form,\n });\n\n const payload = (await response.json().catch(() => null)) as {\n data?: UploadedAttachment;\n error?: { code: string; message: string; field?: string };\n } | null;\n\n if (!response.ok || payload?.error) {\n throw new PidgeonError({\n code: payload?.error?.code ?? 'internal',\n message: payload?.error?.message ?? `Request failed with ${response.status}.`,\n status: response.status,\n field: payload?.error?.field,\n requestId: response.headers.get('x-request-id') ?? undefined,\n });\n }\n\n return payload!.data!;\n },\n };\n\n // --- mailboxes ----------------------------------------------------------\n\n mailboxes = {\n /** The folders of one address, with their counts. */\n list: (identityId: string) =>\n this.#requestPage<Mailbox>('GET', '/mailboxes', { identity_id: identityId }),\n\n /**\n * One folder, a page at a time.\n *\n * `cursor` is the `next_cursor` of the page before it and is opaque —\n * it carries the sort key and the id together, because several messages\n * can share a millisecond. Do not construct one.\n */\n list_folder: (\n folder: Mailbox['folder'],\n identityId: string,\n options: { limit?: number; cursor?: string } = {},\n ) =>\n this.#requestPage<MessageListItem>('GET', `/mailboxes/${folder}`, {\n identity_id: identityId,\n ...options,\n }),\n\n /**\n * What changed since you last looked.\n *\n * Per mailbox, not per folder: a message moving from the inbox to the\n * archive is one event, and the row says where it went. Arrivals are in\n * it too, so this is the only thing you need to poll.\n *\n * `since` is the `sync_cursor` from a listing, or the `next_cursor` from\n * a previous call to this. It is **not** a page cursor — the two are\n * different orders and passing one for the other is refused rather than\n * guessed at.\n *\n * Throws `resync` when the cursor is older than the window in which we\n * keep the record of what was deleted. List the folders again.\n */\n changes: (identityId: string, since: string, options: { limit?: number } = {}) =>\n this.#requestPage<MailboxChange>('GET', '/mailboxes/changes', {\n identity_id: identityId,\n since,\n ...options,\n }),\n\n /**\n * Empty the trash, permanently.\n *\n * Only the trash. It takes attachments and archived originals with it and\n * there is no undo — `messages.undo` replays column values, and these\n * rows are gone.\n *\n * `messages.act(ids, 'delete_forever')` does the same to a list you name,\n * and caps at 200. This needs no ids.\n */\n empty_trash: (identityId: string) =>\n this.#request<{ folder: string; deleted: number }>(\n 'DELETE',\n `/mailboxes/trash?identity_id=${identityId}`,\n ),\n };\n\n // --- workspace ----------------------------------------------------------\n\n workspace = {\n /** Which account this key is for, and which plan sets its limits. */\n get: () => this.#request<Workspace>('GET', '/workspace'),\n /**\n * The people here, so a conversation can be assigned to one of them.\n *\n * Read-only. Inviting somebody is mail to a person and removing somebody\n * hands on their mailboxes; neither is a thing to do without a human\n * deciding it.\n */\n members: () => this.#requestPage<Member>('GET', '/workspace/members', {}),\n };\n\n // --- credentials --------------------------------------------------------\n\n /**\n * A credential per customer, minted by this one.\n *\n * `identity_ids` is required and must be a subset of what this key already\n * reaches — a key cannot delegate reach it does not have. Hand the new key\n * to the tenant it was scoped for, and revoke it when they leave.\n *\n * **Revoking takes the keys it made with it**, transitively. That is what\n * makes rotating a leaked key true: its first useful move would otherwise\n * be to mint a second one and keep it.\n */\n keys = {\n list: () => this.#request<ApiKey[]>('GET', '/api-keys'),\n /** The response carries `token`, once. Store it before you discard it. */\n create: (input: { name: string; identity_ids: string[] }) =>\n this.#request<ApiKey & { token: string }>('POST', '/api-keys', { body: input }),\n /** Revokes it and everything it minted. A key may revoke itself. */\n revoke: (id: string) => this.#request<{ id: string }>('DELETE', `/api-keys/${id}`),\n };\n\n /**\n * Working IMAP and SMTP credentials for one address.\n *\n * What lets you hand a customer a mailbox they can open in Apple Mail. The\n * address you may issue one for is an address this key can already send\n * from — the credential reads the inbox and sends as it, so issuing one is\n * handing over exactly that.\n *\n * `username` and `password` are useless apart: a password presented for the\n * wrong address is a failed login, not a successful one.\n */\n appPasswords = {\n list: (identityId: string) =>\n this.#request<AppPassword[]>('GET', `/app-passwords?identity_id=${identityId}`),\n create: (input: { identity_id: string; name: string }) =>\n this.#request<AppPassword & { username: string; password: string }>(\n 'POST',\n '/app-passwords',\n { body: input },\n ),\n revoke: (id: string) => this.#request<{ id: string }>('DELETE', `/app-passwords/${id}`),\n };\n\n // --- threads ------------------------------------------------------------\n\n threads = {\n list: (\n options: {\n identity_id?: string;\n status?: string;\n /** A person's id, or `'unassigned'` for the queue nobody has taken. */\n assignee_id?: string;\n limit?: number;\n cursor?: string;\n } = {},\n ) => this.#requestPage<Thread>('GET', '/threads', options),\n get: (id: string) => this.#request<Thread>('GET', `/threads/${id}`),\n update: (\n id: string,\n input: {\n status?: 'open' | 'waiting' | 'resolved' | 'human_review';\n owner_type?: 'human' | 'automation' | 'agent';\n /** A person's id to hand it to, or `null` to put it back in the queue. */\n assignee_id?: string | null;\n },\n ) => this.#request<Thread>('PATCH', `/threads/${id}`, { body: input }),\n /** Hand the conversation to a person. The takeover primitive, named. */\n handOff: (id: string) =>\n this.#request<Thread>('PATCH', `/threads/${id}`, {\n body: { owner_type: 'human', status: 'human_review' },\n }),\n };\n\n // --- webhooks -----------------------------------------------------------\n\n webhooks = {\n list: () => this.#request<Webhook[]>('GET', '/webhooks'),\n /** The response carries `secret`, once. Store it before you discard it. */\n create: (input: { url: string; identity_id?: string | null; event_types?: string[] }) =>\n this.#request<Webhook & { secret: string }>('POST', '/webhooks', { body: input }),\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/webhooks/${id}`),\n /**\n * Turn an endpoint off, or back on.\n *\n * We disable one ourselves after enough consecutive failures. Without\n * this you would have to delete and recreate it, which changes the\n * signing secret every receiver is verifying against.\n */\n update: (id: string, input: { status: 'active' | 'disabled' }) =>\n this.#request<Webhook>('PATCH', `/webhooks/${id}`, { body: input }),\n /** What happened to each event we tried to hand this endpoint. */\n deliveries: (webhookId: string, options: { limit?: number } = {}) =>\n this.#request<Delivery[]>(\n 'GET',\n `/deliveries?webhook_id=${webhookId}` +\n (options.limit === undefined ? '' : `&limit=${options.limit}`),\n ),\n /** Queue one up again. Not idempotent — sending twice is the point. */\n replay: (deliveryId: string) =>\n this.#request<{ id: string; status: string }>('POST', `/deliveries/${deliveryId}/replay`),\n };\n\n // --- events -------------------------------------------------------------\n\n events = {\n list: (\n options: { type?: string; identity_id?: string; limit?: number; cursor?: string } = {},\n ) => this.#requestPage<Event>('GET', '/events', options),\n get: (id: string) => this.#request<Event>('GET', `/events/${id}`),\n };\n\n /**\n * Domains, and the one step this API cannot perform for you.\n *\n * We cannot write records in somebody else's zone. What we can do is make\n * the human step **delegable**: `create` hands back the records alongside\n * the domain, so you show them to your own customer in your own interface\n * and never send anybody to a Pidgeon screen. Then poll `verify`, or\n * subscribe to `domain.verified` and do not poll at all.\n *\n * `dns_complete` is not a detail: the DKIM records come from what the mail\n * provider issued, so a list returned before provisioning finished looks\n * complete and is missing three CNAMEs. Publishing that gives you a domain\n * that verifies and cannot sign mail.\n */\n domains = {\n list: () => this.#request<Domain[]>('GET', '/domains'),\n get: (id: string) =>\n this.#request<Domain & { dns_records: DnsRecord[]; dns_complete: boolean }>(\n 'GET',\n `/domains/${id}`,\n ),\n create: (input: { domain: string }) =>\n this.#request<Domain & { dns_records: DnsRecord[]; dns_complete: boolean }>(\n 'POST',\n '/domains',\n { body: input },\n ),\n /** Idempotent and safe to poll. Calling it on a verified domain re-checks. */\n verify: (id: string) =>\n this.#request<{\n verified: boolean;\n mail_configured: boolean;\n message: string;\n dns_records: DnsRecord[];\n dns_complete: boolean;\n }>('POST', `/domains/${id}/verify`),\n /** Takes its addresses, their mail, their rules and their webhooks with it. */\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/domains/${id}`),\n /**\n * The catch-all switch — the one thing about a domain that can change.\n *\n * Catch-all is two gates: this, and a `catch_all` routing rule saying\n * where the mail goes. Turning this on without the rule does nothing, so\n * that turning it off later does not throw away where the mail was going.\n */\n update: (id: string, input: { catch_all_enabled: boolean }) =>\n this.#request<Domain>('PATCH', `/domains/${id}`, { body: input }),\n };\n\n // --- routing ------------------------------------------------------------\n\n /**\n * Where mail to an address goes.\n *\n * The piece that makes per-customer addresses work without a person in the\n * loop: `acme@yourdomain.com` routed into one inbox you already have, or\n * into a webhook with no inbox at all. An address always beats a rule, so a\n * rule never shadows a real address — creating one that would is refused.\n *\n * The destination must be an address you could already send from, because\n * routing an address to a mailbox also lets that mailbox send as it.\n */\n routing = {\n list: (options: { domain_id?: string } = {}) =>\n this.#request<RoutingRule[]>(\n 'GET',\n options.domain_id ? `/routing-rules?domain_id=${options.domain_id}` : '/routing-rules',\n ),\n create: (\n input:\n | {\n match_type: 'address';\n local_part: string;\n destination_identity_id?: string;\n destination_webhook_id?: string;\n }\n | {\n match_type: 'catch_all';\n domain_id: string;\n destination_identity_id?: string;\n destination_webhook_id?: string;\n },\n ) => this.#request<RoutingRule>('POST', '/routing-rules', { body: input }),\n /** Stop a rule firing without forgetting where it pointed. */\n update: (id: string, input: { status: 'active' | 'disabled' }) =>\n this.#request<RoutingRule>('PATCH', `/routing-rules/${id}`, { body: input }),\n delete: (id: string) => this.#request<{ id: string }>('DELETE', `/routing-rules/${id}`),\n };\n\n async #requestPage<T>(\n method: string,\n path: string,\n // Booleans are here for the search filters, which are flags on the wire.\n // `String(false)` is `\"false\"`, which is what the API's schema reads.\n query: Record<string, string | number | boolean | undefined>,\n ): Promise<Page<T>> {\n const url = new URL(`${this.#baseUrl}${path}`);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n\n const response = await this.#fetch(url, {\n method,\n headers: { Authorization: `Bearer ${this.#apiKey}` },\n });\n\n const payload = (await response.json().catch(() => null)) as\n (Page<T> & { error?: { code: string; message: string; field?: string } }) | null;\n\n if (!response.ok || payload?.error) {\n throw new PidgeonError({\n code: payload?.error?.code ?? 'internal',\n message: payload?.error?.message ?? `Request failed with ${response.status}.`,\n status: response.status,\n field: payload?.error?.field,\n requestId: response.headers.get('x-request-id') ?? undefined,\n });\n }\n\n return {\n data: payload?.data ?? [],\n has_more: payload?.has_more ?? false,\n next_cursor: payload?.next_cursor ?? null,\n };\n }\n}\n","/**\n * Verifying a delivery.\n *\n * The only part of this package with a security contract: it is what a\n * customer runs to decide whether a request claiming a message arrived\n * actually came from us. Everything else here is a typed `fetch`.\n *\n * Two properties carry that contract, and both are tested against payloads\n * signed by the server's own implementation — see\n * `tests/unit/sdk-webhook-verifier.test.ts`. Testing this file against\n * itself would prove only that it agrees with itself, which is exactly the\n * bug two implementations of one scheme produce.\n */\n\nexport const SIGNATURE_HEADER = 'x-pidgeon-signature';\nexport const TIMESTAMP_HEADER = 'x-pidgeon-timestamp';\n\nexport interface WebhookEnvelope {\n id: string;\n type: string;\n created_at: string;\n data: Record<string, unknown>;\n}\n\n/**\n * Verifies a delivery and returns its payload.\n *\n * Throws rather than returning false, because the one thing this must not\n * allow is a caller who forgets to check the boolean. Uses `crypto.subtle`,\n * so it works unchanged in Node, Bun, Deno, Workers and Vercel functions.\n *\n * `rawBody` must be the exact bytes we sent. Re-serialising parsed JSON\n * changes key order and whitespace, and the signature will not match — read\n * the body as text before you parse it.\n */\nexport async function verifyWebhook(input: {\n rawBody: string;\n headers: Headers | Record<string, string | undefined>;\n secret: string;\n toleranceSeconds?: number;\n now?: Date;\n}): Promise<WebhookEnvelope> {\n const get = (name: string): string | undefined =>\n input.headers instanceof Headers\n ? (input.headers.get(name) ?? undefined)\n : (input.headers[name] ?? input.headers[name.toLowerCase()]);\n\n const timestamp = get(TIMESTAMP_HEADER);\n const signature = get(SIGNATURE_HEADER);\n\n if (!timestamp || !signature) throw new Error('This request is not signed.');\n\n const seconds = Number(timestamp);\n if (!Number.isFinite(seconds)) throw new Error('The signature timestamp is not a number.');\n\n const now = Math.floor((input.now ?? new Date()).getTime() / 1000);\n const tolerance = input.toleranceSeconds ?? 300;\n\n if (Math.abs(now - seconds) > tolerance) {\n throw new Error('This request is too old to trust. It may be a replay.');\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(input.secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const mac = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(`${seconds}.${input.rawBody}`),\n );\n\n const expected = `v1=${[...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`;\n\n if (!constantTimeEqual(expected, signature)) {\n throw new Error('The signature does not match.');\n }\n\n return JSON.parse(input.rawBody) as WebhookEnvelope;\n}\n\n/** Length-independent compare. Not `===`, which returns on the first byte. */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n\n let difference = 0;\n for (let index = 0; index < a.length; index += 1) {\n difference |= a.charCodeAt(index) ^ b.charCodeAt(index);\n }\n\n return difference === 0;\n}\n"],"mappings":"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","names":[]}