@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 +51 -0
- package/dist/index.cjs +250 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +495 -8
- package/dist/index.d.ts +495 -8
- package/dist/index.js +250 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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}`);
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"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","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":[]}
|