@pidgeon-ai/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +130 -0
- package/dist/index.cjs +210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +302 -0
- package/dist/index.d.ts +302 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Versioned independently of the application. The API is `/v1`; this package
|
|
4
|
+
is `0.x`. They are different contracts with different audiences, and tying
|
|
5
|
+
the two would publish a release for every unrelated product deploy.
|
|
6
|
+
|
|
7
|
+
Semver applies to **this package's own surface** — the exported types, the
|
|
8
|
+
method signatures, the error shape — not to the API behind it.
|
|
9
|
+
|
|
10
|
+
## Unreleased
|
|
11
|
+
|
|
12
|
+
## 0.1.0
|
|
13
|
+
|
|
14
|
+
First release.
|
|
15
|
+
|
|
16
|
+
- `Pidgeon` — a typed client over `/v1`: `identities`, `messages`,
|
|
17
|
+
`attachments`, `threads`, `webhooks`, `events`, `domains`.
|
|
18
|
+
- `verifyWebhook` — the receiving half of the signature scheme, tested
|
|
19
|
+
against payloads signed by the server's own implementation.
|
|
20
|
+
- `PidgeonError` — one error class, carrying the API's `code`, HTTP
|
|
21
|
+
`status`, the offending `field` on a validation failure, and the request
|
|
22
|
+
id.
|
|
23
|
+
- ESM and CommonJS builds with per-condition type declarations. Zero
|
|
24
|
+
runtime dependencies; needs only `fetch` and `crypto.subtle`.
|
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @pidgeon-ai/sdk
|
|
2
|
+
|
|
3
|
+
A thin TypeScript client for the [Pidgeon](https://pidgeon.ai) email API.
|
|
4
|
+
Typed `fetch` wrappers, one error class, and a webhook verifier.
|
|
5
|
+
|
|
6
|
+
**Zero dependencies.** Needs `fetch` and `crypto.subtle`, so it runs on
|
|
7
|
+
Node 18+, Bun, Deno, Cloudflare Workers and Vercel functions. ESM and
|
|
8
|
+
CommonJS.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @pidgeon-ai/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Receiving mail
|
|
15
|
+
|
|
16
|
+
Mail arriving at an address your code owns becomes an event, and your
|
|
17
|
+
endpoint is called with a signed payload.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Pidgeon, verifyWebhook } from '@pidgeon-ai/sdk';
|
|
21
|
+
|
|
22
|
+
const pidgeon = new Pidgeon({ apiKey: process.env.PIDGEON_API_KEY! });
|
|
23
|
+
|
|
24
|
+
export async function POST(request: Request) {
|
|
25
|
+
// Verify first. An unverified payload is an unauthenticated POST that
|
|
26
|
+
// claims a message arrived.
|
|
27
|
+
const event = await verifyWebhook({
|
|
28
|
+
rawBody: await request.text(),
|
|
29
|
+
headers: request.headers,
|
|
30
|
+
secret: process.env.PIDGEON_WEBHOOK_SECRET!,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (event.type !== 'message.received') return new Response(null, { status: 204 });
|
|
34
|
+
|
|
35
|
+
// The event carries ids, never a body. Exchange one for the content.
|
|
36
|
+
const mail = await pidgeon.messages.get(event.data.message_id as string);
|
|
37
|
+
|
|
38
|
+
await pidgeon.messages.send({
|
|
39
|
+
from: mail.to[0].email,
|
|
40
|
+
to: mail.from.email,
|
|
41
|
+
subject: `Re: ${mail.subject}`,
|
|
42
|
+
text: 'Got it — someone will be in touch.',
|
|
43
|
+
// Keeps the conversation together in the recipient's client too.
|
|
44
|
+
in_reply_to: mail.id,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return new Response(null, { status: 204 });
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`rawBody` must be the exact bytes we sent. Parse the JSON _after_
|
|
52
|
+
verifying — re-serialising changes key order and the signature will not
|
|
53
|
+
match.
|
|
54
|
+
|
|
55
|
+
`verifyWebhook` throws rather than returning a boolean, because the failure
|
|
56
|
+
mode of a boolean is a caller who forgets to check it. It rejects a
|
|
57
|
+
timestamp outside tolerance (five minutes by default) before it looks at
|
|
58
|
+
the signature, and compares in constant time.
|
|
59
|
+
|
|
60
|
+
## Handing a conversation to a person
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
await pidgeon.threads.handOff(threadId);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It appears in the web inbox, and `thread.updated` tells whatever was
|
|
67
|
+
driving the conversation that it no longer is.
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
Every failure is a `PidgeonError` carrying the API's own `code`, the HTTP
|
|
72
|
+
`status`, the offending `field` on a validation failure, and the request id
|
|
73
|
+
to quote in a support conversation.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { PidgeonError } from '@pidgeon-ai/sdk';
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await pidgeon.messages.send({ from, to, text });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof PidgeonError && error.code === 'rate_limited') {
|
|
82
|
+
// Back off. Deliberately not done for you — see below.
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`code` is one of `validation`, `unauthenticated`, `entitlement`,
|
|
89
|
+
`forbidden`, `not_found`, `conflict`, `rate_limited`, `internal`.
|
|
90
|
+
|
|
91
|
+
## Pagination
|
|
92
|
+
|
|
93
|
+
List endpoints return a page, not an iterator.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
let cursor: string | null = null;
|
|
97
|
+
|
|
98
|
+
do {
|
|
99
|
+
const page = await pidgeon.events.list({ limit: 50, cursor: cursor ?? undefined });
|
|
100
|
+
for (const event of page.data) handle(event);
|
|
101
|
+
cursor = page.next_cursor;
|
|
102
|
+
} while (cursor);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## What this deliberately does not do
|
|
106
|
+
|
|
107
|
+
No retry policy, no queue, no caching, no auto-paginating iterator. Those
|
|
108
|
+
are decisions that belong to your application — an SDK that makes them for
|
|
109
|
+
you is one you eventually work around, and an iterator that hides
|
|
110
|
+
pagination also hides the rate limit pagination is subject to.
|
|
111
|
+
|
|
112
|
+
## Options
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
new Pidgeon({
|
|
116
|
+
apiKey: process.env.PIDGEON_API_KEY!,
|
|
117
|
+
// Point at a local dev server, or a preview deployment.
|
|
118
|
+
baseUrl: 'http://localhost:3000/api/v1',
|
|
119
|
+
// Inject a transport. Useful in tests.
|
|
120
|
+
fetch: myFetch,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Versioning
|
|
125
|
+
|
|
126
|
+
Versioned independently of the API. The API is `/v1`; this package is
|
|
127
|
+
`0.x`, and semver applies to its own surface — the exported types, the
|
|
128
|
+
method signatures, the error shape. See `CHANGELOG.md`.
|
|
129
|
+
|
|
130
|
+
MIT.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Pidgeon: () => Pidgeon,
|
|
24
|
+
PidgeonError: () => PidgeonError,
|
|
25
|
+
SIGNATURE_HEADER: () => SIGNATURE_HEADER,
|
|
26
|
+
TIMESTAMP_HEADER: () => TIMESTAMP_HEADER,
|
|
27
|
+
verifyWebhook: () => verifyWebhook
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/client.ts
|
|
32
|
+
var PidgeonError = class extends Error {
|
|
33
|
+
code;
|
|
34
|
+
status;
|
|
35
|
+
field;
|
|
36
|
+
requestId;
|
|
37
|
+
constructor(input) {
|
|
38
|
+
super(input.message);
|
|
39
|
+
this.name = "PidgeonError";
|
|
40
|
+
this.code = input.code;
|
|
41
|
+
this.status = input.status;
|
|
42
|
+
this.field = input.field;
|
|
43
|
+
this.requestId = input.requestId;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var DEFAULT_BASE_URL = "https://pidgeon.ai/api/v1";
|
|
47
|
+
var Pidgeon = class {
|
|
48
|
+
#apiKey;
|
|
49
|
+
#baseUrl;
|
|
50
|
+
#fetch;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
if (!options.apiKey) throw new Error("An API key is required.");
|
|
53
|
+
this.#apiKey = options.apiKey;
|
|
54
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
55
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
56
|
+
}
|
|
57
|
+
async #request(method, path, options = {}) {
|
|
58
|
+
const url = new URL(`${this.#baseUrl}${path}`);
|
|
59
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
60
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
61
|
+
}
|
|
62
|
+
const response = await this.#fetch(url, {
|
|
63
|
+
method,
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
66
|
+
...options.body ? { "Content-Type": "application/json" } : {}
|
|
67
|
+
},
|
|
68
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
69
|
+
});
|
|
70
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
71
|
+
const payload = await response.json().catch(() => null);
|
|
72
|
+
if (!response.ok || payload?.error) {
|
|
73
|
+
throw new PidgeonError({
|
|
74
|
+
code: payload?.error?.code ?? "internal",
|
|
75
|
+
message: payload?.error?.message ?? `Request failed with ${response.status}.`,
|
|
76
|
+
status: response.status,
|
|
77
|
+
field: payload?.error?.field,
|
|
78
|
+
requestId
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return payload?.data;
|
|
82
|
+
}
|
|
83
|
+
// --- identities ---------------------------------------------------------
|
|
84
|
+
identities = {
|
|
85
|
+
list: () => this.#request("GET", "/identities"),
|
|
86
|
+
get: (id) => this.#request("GET", `/identities/${id}`),
|
|
87
|
+
create: (input) => this.#request("POST", "/identities", { body: input }),
|
|
88
|
+
update: (id, input) => this.#request("PATCH", `/identities/${id}`, { body: input }),
|
|
89
|
+
delete: (id) => this.#request("DELETE", `/identities/${id}`)
|
|
90
|
+
};
|
|
91
|
+
// --- messages -----------------------------------------------------------
|
|
92
|
+
messages = {
|
|
93
|
+
get: (id) => this.#request("GET", `/messages/${id}`),
|
|
94
|
+
listInThread: (threadId, options = {}) => this.#requestPage("GET", "/messages", {
|
|
95
|
+
thread_id: threadId,
|
|
96
|
+
limit: options.limit
|
|
97
|
+
}),
|
|
98
|
+
send: (input) => this.#request(
|
|
99
|
+
"POST",
|
|
100
|
+
"/messages",
|
|
101
|
+
{ body: input }
|
|
102
|
+
)
|
|
103
|
+
};
|
|
104
|
+
attachments = {
|
|
105
|
+
/** A short-lived signed URL. Fetch it when you need it, don't store it. */
|
|
106
|
+
downloadUrl: (id) => this.#request("GET", `/attachments/${id}`)
|
|
107
|
+
};
|
|
108
|
+
// --- threads ------------------------------------------------------------
|
|
109
|
+
threads = {
|
|
110
|
+
list: (options = {}) => this.#requestPage("GET", "/threads", options),
|
|
111
|
+
get: (id) => this.#request("GET", `/threads/${id}`),
|
|
112
|
+
update: (id, input) => this.#request("PATCH", `/threads/${id}`, { body: input }),
|
|
113
|
+
/** Hand the conversation to a person. The takeover primitive, named. */
|
|
114
|
+
handOff: (id) => this.#request("PATCH", `/threads/${id}`, {
|
|
115
|
+
body: { owner_type: "human", status: "human_review" }
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
// --- webhooks -----------------------------------------------------------
|
|
119
|
+
webhooks = {
|
|
120
|
+
list: () => this.#request("GET", "/webhooks"),
|
|
121
|
+
/** The response carries `secret`, once. Store it before you discard it. */
|
|
122
|
+
create: (input) => this.#request("POST", "/webhooks", { body: input }),
|
|
123
|
+
delete: (id) => this.#request("DELETE", `/webhooks/${id}`)
|
|
124
|
+
};
|
|
125
|
+
// --- events -------------------------------------------------------------
|
|
126
|
+
events = {
|
|
127
|
+
list: (options = {}) => this.#requestPage("GET", "/events", options),
|
|
128
|
+
get: (id) => this.#request("GET", `/events/${id}`)
|
|
129
|
+
};
|
|
130
|
+
domains = {
|
|
131
|
+
/** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
|
|
132
|
+
list: () => this.#request("GET", "/domains")
|
|
133
|
+
};
|
|
134
|
+
async #requestPage(method, path, query) {
|
|
135
|
+
const url = new URL(`${this.#baseUrl}${path}`);
|
|
136
|
+
for (const [key, value] of Object.entries(query)) {
|
|
137
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
138
|
+
}
|
|
139
|
+
const response = await this.#fetch(url, {
|
|
140
|
+
method,
|
|
141
|
+
headers: { Authorization: `Bearer ${this.#apiKey}` }
|
|
142
|
+
});
|
|
143
|
+
const payload = await response.json().catch(() => null);
|
|
144
|
+
if (!response.ok || payload?.error) {
|
|
145
|
+
throw new PidgeonError({
|
|
146
|
+
code: payload?.error?.code ?? "internal",
|
|
147
|
+
message: payload?.error?.message ?? `Request failed with ${response.status}.`,
|
|
148
|
+
status: response.status,
|
|
149
|
+
field: payload?.error?.field,
|
|
150
|
+
requestId: response.headers.get("x-request-id") ?? void 0
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
data: payload?.data ?? [],
|
|
155
|
+
has_more: payload?.has_more ?? false,
|
|
156
|
+
next_cursor: payload?.next_cursor ?? null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/webhooks.ts
|
|
162
|
+
var SIGNATURE_HEADER = "x-pidgeon-signature";
|
|
163
|
+
var TIMESTAMP_HEADER = "x-pidgeon-timestamp";
|
|
164
|
+
async function verifyWebhook(input) {
|
|
165
|
+
const get = (name) => input.headers instanceof Headers ? input.headers.get(name) ?? void 0 : input.headers[name] ?? input.headers[name.toLowerCase()];
|
|
166
|
+
const timestamp = get(TIMESTAMP_HEADER);
|
|
167
|
+
const signature = get(SIGNATURE_HEADER);
|
|
168
|
+
if (!timestamp || !signature) throw new Error("This request is not signed.");
|
|
169
|
+
const seconds = Number(timestamp);
|
|
170
|
+
if (!Number.isFinite(seconds)) throw new Error("The signature timestamp is not a number.");
|
|
171
|
+
const now = Math.floor((input.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
172
|
+
const tolerance = input.toleranceSeconds ?? 300;
|
|
173
|
+
if (Math.abs(now - seconds) > tolerance) {
|
|
174
|
+
throw new Error("This request is too old to trust. It may be a replay.");
|
|
175
|
+
}
|
|
176
|
+
const key = await crypto.subtle.importKey(
|
|
177
|
+
"raw",
|
|
178
|
+
new TextEncoder().encode(input.secret),
|
|
179
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
180
|
+
false,
|
|
181
|
+
["sign"]
|
|
182
|
+
);
|
|
183
|
+
const mac = await crypto.subtle.sign(
|
|
184
|
+
"HMAC",
|
|
185
|
+
key,
|
|
186
|
+
new TextEncoder().encode(`${seconds}.${input.rawBody}`)
|
|
187
|
+
);
|
|
188
|
+
const expected = `v1=${[...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
189
|
+
if (!constantTimeEqual(expected, signature)) {
|
|
190
|
+
throw new Error("The signature does not match.");
|
|
191
|
+
}
|
|
192
|
+
return JSON.parse(input.rawBody);
|
|
193
|
+
}
|
|
194
|
+
function constantTimeEqual(a, b) {
|
|
195
|
+
if (a.length !== b.length) return false;
|
|
196
|
+
let difference = 0;
|
|
197
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
198
|
+
difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
199
|
+
}
|
|
200
|
+
return difference === 0;
|
|
201
|
+
}
|
|
202
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
203
|
+
0 && (module.exports = {
|
|
204
|
+
Pidgeon,
|
|
205
|
+
PidgeonError,
|
|
206
|
+
SIGNATURE_HEADER,
|
|
207
|
+
TIMESTAMP_HEADER,
|
|
208
|
+
verifyWebhook
|
|
209
|
+
});
|
|
210
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire format.
|
|
3
|
+
*
|
|
4
|
+
* **This is the single definition of what the API returns**, and it lives
|
|
5
|
+
* here rather than in the application on purpose: the wire format *is* the
|
|
6
|
+
* public contract, and this package is the artifact that contract is
|
|
7
|
+
* published as.
|
|
8
|
+
*
|
|
9
|
+
* `src/lib/api/dto.ts` imports these types — with `import type`, so nothing
|
|
10
|
+
* of this package reaches the application at runtime — and every mapper
|
|
11
|
+
* there is checked against them. A field renamed in the application is a
|
|
12
|
+
* compile error in the application, immediately, rather than a silent lie
|
|
13
|
+
* in a customer's editor a release later. That was the alternative worth
|
|
14
|
+
* avoiding: two hand-maintained copies of the same shapes, which is what
|
|
15
|
+
* this replaced.
|
|
16
|
+
*
|
|
17
|
+
* Conventions, and they are not negotiable per-field: `snake_case`,
|
|
18
|
+
* ISO-8601 timestamps, ids as opaque strings. Nothing here ever carries a
|
|
19
|
+
* secret, a hash, a spam score or an internal provider id.
|
|
20
|
+
*/
|
|
21
|
+
interface Address {
|
|
22
|
+
email: string;
|
|
23
|
+
name?: string | null;
|
|
24
|
+
}
|
|
25
|
+
interface Attachment {
|
|
26
|
+
id: string;
|
|
27
|
+
filename: string;
|
|
28
|
+
content_type: string;
|
|
29
|
+
size: number;
|
|
30
|
+
/** False when there is no object store behind the metadata. */
|
|
31
|
+
downloadable: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface Identity {
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
local_part: string;
|
|
37
|
+
domain: string;
|
|
38
|
+
display_name: string | null;
|
|
39
|
+
/** `human`, `team`, `application`, `workflow` or `agent`. */
|
|
40
|
+
type: string;
|
|
41
|
+
/** Who answers: `human`, `automated` or `hybrid`. */
|
|
42
|
+
mode: string;
|
|
43
|
+
status: string;
|
|
44
|
+
capabilities: Record<string, boolean>;
|
|
45
|
+
/** False for an address that turns mail into events and stores nothing. */
|
|
46
|
+
has_inbox: boolean;
|
|
47
|
+
routed_address_count: number;
|
|
48
|
+
created_at: string;
|
|
49
|
+
}
|
|
50
|
+
interface Message {
|
|
51
|
+
id: string;
|
|
52
|
+
thread_id: string;
|
|
53
|
+
identity: string | null;
|
|
54
|
+
direction: string;
|
|
55
|
+
from: Address;
|
|
56
|
+
to: Address[];
|
|
57
|
+
cc: Address[];
|
|
58
|
+
subject: string | null;
|
|
59
|
+
/** The envelope recipient, which is not always in `to`. */
|
|
60
|
+
delivered_to: string | null;
|
|
61
|
+
text: string | null;
|
|
62
|
+
/** Sanitised server-side. Our policy travels with the content. */
|
|
63
|
+
html: string;
|
|
64
|
+
is_read: boolean;
|
|
65
|
+
is_spam: boolean;
|
|
66
|
+
attachments: Attachment[];
|
|
67
|
+
created_at: string;
|
|
68
|
+
}
|
|
69
|
+
interface Thread {
|
|
70
|
+
id: string;
|
|
71
|
+
identity: string;
|
|
72
|
+
subject: string | null;
|
|
73
|
+
status: 'open' | 'waiting' | 'resolved' | 'human_review';
|
|
74
|
+
owner_type: 'human' | 'automation' | 'agent';
|
|
75
|
+
message_count: number;
|
|
76
|
+
last_message_at: string | null;
|
|
77
|
+
created_at: string;
|
|
78
|
+
}
|
|
79
|
+
interface Webhook {
|
|
80
|
+
id: string;
|
|
81
|
+
url: string;
|
|
82
|
+
identity: string | null;
|
|
83
|
+
event_types: string[];
|
|
84
|
+
status: string;
|
|
85
|
+
consecutive_failures: number;
|
|
86
|
+
last_delivered_at: string | null;
|
|
87
|
+
last_failed_at: string | null;
|
|
88
|
+
created_at: string;
|
|
89
|
+
}
|
|
90
|
+
interface Event {
|
|
91
|
+
id: string;
|
|
92
|
+
type: string;
|
|
93
|
+
identity: string | null;
|
|
94
|
+
message_id: string | null;
|
|
95
|
+
thread_id: string | null;
|
|
96
|
+
/** Ids and metadata. An event never carries a message body. */
|
|
97
|
+
data: Record<string, unknown>;
|
|
98
|
+
created_at: string;
|
|
99
|
+
}
|
|
100
|
+
interface RoutingRule {
|
|
101
|
+
id: string;
|
|
102
|
+
/** The address or pattern this rule matches. */
|
|
103
|
+
match: string;
|
|
104
|
+
/** `exact` for an address, `catch_all` for the domain's fallback. */
|
|
105
|
+
match_type: string;
|
|
106
|
+
/** `identity` for an inbox, `webhook` for a URL. */
|
|
107
|
+
destination_type: string;
|
|
108
|
+
destination: string;
|
|
109
|
+
status: string;
|
|
110
|
+
domain: string;
|
|
111
|
+
created_at: string;
|
|
112
|
+
}
|
|
113
|
+
interface Domain {
|
|
114
|
+
id: string;
|
|
115
|
+
domain: string;
|
|
116
|
+
status: string;
|
|
117
|
+
catch_all_enabled: boolean;
|
|
118
|
+
identity_count: number;
|
|
119
|
+
verified_at: string | null;
|
|
120
|
+
created_at: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A page of results.
|
|
124
|
+
*
|
|
125
|
+
* Deliberately not an async iterator. Hiding pagination hides the rate
|
|
126
|
+
* limit that pagination is subject to, and the application calling us is
|
|
127
|
+
* better placed to decide what to do when it is hit.
|
|
128
|
+
*/
|
|
129
|
+
interface Page<T> {
|
|
130
|
+
data: T[];
|
|
131
|
+
has_more: boolean;
|
|
132
|
+
next_cursor: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The Pidgeon client.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately thin: typed wrappers over `fetch`, one error class, and
|
|
139
|
+
* nothing else. No retry policy, no queue, no caching, no ambient state —
|
|
140
|
+
* those are decisions that belong to the application, and an SDK that makes
|
|
141
|
+
* them for you is an SDK you eventually have to work around.
|
|
142
|
+
*
|
|
143
|
+
* Zero dependencies. The only runtime requirements are `fetch` and
|
|
144
|
+
* `crypto.subtle`, both standard in Node 18+, Bun, Deno and every edge
|
|
145
|
+
* runtime.
|
|
146
|
+
*/
|
|
147
|
+
interface PidgeonOptions {
|
|
148
|
+
apiKey: string;
|
|
149
|
+
/** Defaults to the hosted API. Point this at a local dev server to test. */
|
|
150
|
+
baseUrl?: string;
|
|
151
|
+
/** Injected in tests. Defaults to the global. */
|
|
152
|
+
fetch?: typeof globalThis.fetch;
|
|
153
|
+
}
|
|
154
|
+
declare class PidgeonError extends Error {
|
|
155
|
+
readonly code: string;
|
|
156
|
+
readonly status: number;
|
|
157
|
+
readonly field?: string;
|
|
158
|
+
readonly requestId?: string;
|
|
159
|
+
constructor(input: {
|
|
160
|
+
code: string;
|
|
161
|
+
message: string;
|
|
162
|
+
status: number;
|
|
163
|
+
field?: string;
|
|
164
|
+
requestId?: string;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
declare class Pidgeon {
|
|
168
|
+
#private;
|
|
169
|
+
constructor(options: PidgeonOptions);
|
|
170
|
+
identities: {
|
|
171
|
+
list: () => Promise<Identity[]>;
|
|
172
|
+
get: (id: string) => Promise<Identity>;
|
|
173
|
+
create: (input: {
|
|
174
|
+
domain_id: string;
|
|
175
|
+
local_part: string;
|
|
176
|
+
display_name?: string | null;
|
|
177
|
+
type?: "human" | "team" | "application" | "workflow" | "agent";
|
|
178
|
+
mode?: "human" | "automated" | "hybrid";
|
|
179
|
+
}) => Promise<Identity>;
|
|
180
|
+
update: (id: string, input: {
|
|
181
|
+
display_name?: string | null;
|
|
182
|
+
mode?: "human" | "automated" | "hybrid";
|
|
183
|
+
capabilities?: Record<string, boolean>;
|
|
184
|
+
status?: "active" | "suspended" | "archived";
|
|
185
|
+
}) => Promise<Identity>;
|
|
186
|
+
delete: (id: string) => Promise<{
|
|
187
|
+
id: string;
|
|
188
|
+
}>;
|
|
189
|
+
};
|
|
190
|
+
messages: {
|
|
191
|
+
get: (id: string) => Promise<Message>;
|
|
192
|
+
listInThread: (threadId: string, options?: {
|
|
193
|
+
limit?: number;
|
|
194
|
+
}) => Promise<Page<Message>>;
|
|
195
|
+
send: (input: {
|
|
196
|
+
from: string;
|
|
197
|
+
to: string | string[];
|
|
198
|
+
cc?: string | string[];
|
|
199
|
+
bcc?: string | string[];
|
|
200
|
+
subject?: string;
|
|
201
|
+
text?: string;
|
|
202
|
+
in_reply_to?: string | null;
|
|
203
|
+
}) => Promise<{
|
|
204
|
+
id: string;
|
|
205
|
+
thread_id: string;
|
|
206
|
+
rejected: {
|
|
207
|
+
address: string;
|
|
208
|
+
}[];
|
|
209
|
+
}>;
|
|
210
|
+
};
|
|
211
|
+
attachments: {
|
|
212
|
+
/** A short-lived signed URL. Fetch it when you need it, don't store it. */
|
|
213
|
+
downloadUrl: (id: string) => Promise<{
|
|
214
|
+
id: string;
|
|
215
|
+
url: string;
|
|
216
|
+
}>;
|
|
217
|
+
};
|
|
218
|
+
threads: {
|
|
219
|
+
list: (options?: {
|
|
220
|
+
identity_id?: string;
|
|
221
|
+
status?: string;
|
|
222
|
+
limit?: number;
|
|
223
|
+
cursor?: string;
|
|
224
|
+
}) => Promise<Page<Thread>>;
|
|
225
|
+
get: (id: string) => Promise<Thread>;
|
|
226
|
+
update: (id: string, input: {
|
|
227
|
+
status?: "open" | "waiting" | "resolved" | "human_review";
|
|
228
|
+
owner_type?: "human" | "automation" | "agent";
|
|
229
|
+
}) => Promise<Thread>;
|
|
230
|
+
/** Hand the conversation to a person. The takeover primitive, named. */
|
|
231
|
+
handOff: (id: string) => Promise<Thread>;
|
|
232
|
+
};
|
|
233
|
+
webhooks: {
|
|
234
|
+
list: () => Promise<Webhook[]>;
|
|
235
|
+
/** The response carries `secret`, once. Store it before you discard it. */
|
|
236
|
+
create: (input: {
|
|
237
|
+
url: string;
|
|
238
|
+
identity_id?: string | null;
|
|
239
|
+
event_types?: string[];
|
|
240
|
+
}) => Promise<Webhook & {
|
|
241
|
+
secret: string;
|
|
242
|
+
}>;
|
|
243
|
+
delete: (id: string) => Promise<{
|
|
244
|
+
id: string;
|
|
245
|
+
}>;
|
|
246
|
+
};
|
|
247
|
+
events: {
|
|
248
|
+
list: (options?: {
|
|
249
|
+
type?: string;
|
|
250
|
+
identity_id?: string;
|
|
251
|
+
limit?: number;
|
|
252
|
+
cursor?: string;
|
|
253
|
+
}) => Promise<Page<Event>>;
|
|
254
|
+
get: (id: string) => Promise<Event>;
|
|
255
|
+
};
|
|
256
|
+
domains: {
|
|
257
|
+
/** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
|
|
258
|
+
list: () => Promise<Domain[]>;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verifying a delivery.
|
|
264
|
+
*
|
|
265
|
+
* The only part of this package with a security contract: it is what a
|
|
266
|
+
* customer runs to decide whether a request claiming a message arrived
|
|
267
|
+
* actually came from us. Everything else here is a typed `fetch`.
|
|
268
|
+
*
|
|
269
|
+
* Two properties carry that contract, and both are tested against payloads
|
|
270
|
+
* signed by the server's own implementation — see
|
|
271
|
+
* `tests/unit/sdk-webhook-verifier.test.ts`. Testing this file against
|
|
272
|
+
* itself would prove only that it agrees with itself, which is exactly the
|
|
273
|
+
* bug two implementations of one scheme produce.
|
|
274
|
+
*/
|
|
275
|
+
declare const SIGNATURE_HEADER = "x-pidgeon-signature";
|
|
276
|
+
declare const TIMESTAMP_HEADER = "x-pidgeon-timestamp";
|
|
277
|
+
interface WebhookEnvelope {
|
|
278
|
+
id: string;
|
|
279
|
+
type: string;
|
|
280
|
+
created_at: string;
|
|
281
|
+
data: Record<string, unknown>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Verifies a delivery and returns its payload.
|
|
285
|
+
*
|
|
286
|
+
* Throws rather than returning false, because the one thing this must not
|
|
287
|
+
* allow is a caller who forgets to check the boolean. Uses `crypto.subtle`,
|
|
288
|
+
* so it works unchanged in Node, Bun, Deno, Workers and Vercel functions.
|
|
289
|
+
*
|
|
290
|
+
* `rawBody` must be the exact bytes we sent. Re-serialising parsed JSON
|
|
291
|
+
* changes key order and whitespace, and the signature will not match — read
|
|
292
|
+
* the body as text before you parse it.
|
|
293
|
+
*/
|
|
294
|
+
declare function verifyWebhook(input: {
|
|
295
|
+
rawBody: string;
|
|
296
|
+
headers: Headers | Record<string, string | undefined>;
|
|
297
|
+
secret: string;
|
|
298
|
+
toleranceSeconds?: number;
|
|
299
|
+
now?: Date;
|
|
300
|
+
}): Promise<WebhookEnvelope>;
|
|
301
|
+
|
|
302
|
+
export { type Address, type Attachment, type Domain, type Event, type Identity, type Message, type Page, Pidgeon, PidgeonError, type PidgeonOptions, type RoutingRule, SIGNATURE_HEADER, TIMESTAMP_HEADER, type Thread, type Webhook, type WebhookEnvelope, verifyWebhook };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire format.
|
|
3
|
+
*
|
|
4
|
+
* **This is the single definition of what the API returns**, and it lives
|
|
5
|
+
* here rather than in the application on purpose: the wire format *is* the
|
|
6
|
+
* public contract, and this package is the artifact that contract is
|
|
7
|
+
* published as.
|
|
8
|
+
*
|
|
9
|
+
* `src/lib/api/dto.ts` imports these types — with `import type`, so nothing
|
|
10
|
+
* of this package reaches the application at runtime — and every mapper
|
|
11
|
+
* there is checked against them. A field renamed in the application is a
|
|
12
|
+
* compile error in the application, immediately, rather than a silent lie
|
|
13
|
+
* in a customer's editor a release later. That was the alternative worth
|
|
14
|
+
* avoiding: two hand-maintained copies of the same shapes, which is what
|
|
15
|
+
* this replaced.
|
|
16
|
+
*
|
|
17
|
+
* Conventions, and they are not negotiable per-field: `snake_case`,
|
|
18
|
+
* ISO-8601 timestamps, ids as opaque strings. Nothing here ever carries a
|
|
19
|
+
* secret, a hash, a spam score or an internal provider id.
|
|
20
|
+
*/
|
|
21
|
+
interface Address {
|
|
22
|
+
email: string;
|
|
23
|
+
name?: string | null;
|
|
24
|
+
}
|
|
25
|
+
interface Attachment {
|
|
26
|
+
id: string;
|
|
27
|
+
filename: string;
|
|
28
|
+
content_type: string;
|
|
29
|
+
size: number;
|
|
30
|
+
/** False when there is no object store behind the metadata. */
|
|
31
|
+
downloadable: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface Identity {
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
local_part: string;
|
|
37
|
+
domain: string;
|
|
38
|
+
display_name: string | null;
|
|
39
|
+
/** `human`, `team`, `application`, `workflow` or `agent`. */
|
|
40
|
+
type: string;
|
|
41
|
+
/** Who answers: `human`, `automated` or `hybrid`. */
|
|
42
|
+
mode: string;
|
|
43
|
+
status: string;
|
|
44
|
+
capabilities: Record<string, boolean>;
|
|
45
|
+
/** False for an address that turns mail into events and stores nothing. */
|
|
46
|
+
has_inbox: boolean;
|
|
47
|
+
routed_address_count: number;
|
|
48
|
+
created_at: string;
|
|
49
|
+
}
|
|
50
|
+
interface Message {
|
|
51
|
+
id: string;
|
|
52
|
+
thread_id: string;
|
|
53
|
+
identity: string | null;
|
|
54
|
+
direction: string;
|
|
55
|
+
from: Address;
|
|
56
|
+
to: Address[];
|
|
57
|
+
cc: Address[];
|
|
58
|
+
subject: string | null;
|
|
59
|
+
/** The envelope recipient, which is not always in `to`. */
|
|
60
|
+
delivered_to: string | null;
|
|
61
|
+
text: string | null;
|
|
62
|
+
/** Sanitised server-side. Our policy travels with the content. */
|
|
63
|
+
html: string;
|
|
64
|
+
is_read: boolean;
|
|
65
|
+
is_spam: boolean;
|
|
66
|
+
attachments: Attachment[];
|
|
67
|
+
created_at: string;
|
|
68
|
+
}
|
|
69
|
+
interface Thread {
|
|
70
|
+
id: string;
|
|
71
|
+
identity: string;
|
|
72
|
+
subject: string | null;
|
|
73
|
+
status: 'open' | 'waiting' | 'resolved' | 'human_review';
|
|
74
|
+
owner_type: 'human' | 'automation' | 'agent';
|
|
75
|
+
message_count: number;
|
|
76
|
+
last_message_at: string | null;
|
|
77
|
+
created_at: string;
|
|
78
|
+
}
|
|
79
|
+
interface Webhook {
|
|
80
|
+
id: string;
|
|
81
|
+
url: string;
|
|
82
|
+
identity: string | null;
|
|
83
|
+
event_types: string[];
|
|
84
|
+
status: string;
|
|
85
|
+
consecutive_failures: number;
|
|
86
|
+
last_delivered_at: string | null;
|
|
87
|
+
last_failed_at: string | null;
|
|
88
|
+
created_at: string;
|
|
89
|
+
}
|
|
90
|
+
interface Event {
|
|
91
|
+
id: string;
|
|
92
|
+
type: string;
|
|
93
|
+
identity: string | null;
|
|
94
|
+
message_id: string | null;
|
|
95
|
+
thread_id: string | null;
|
|
96
|
+
/** Ids and metadata. An event never carries a message body. */
|
|
97
|
+
data: Record<string, unknown>;
|
|
98
|
+
created_at: string;
|
|
99
|
+
}
|
|
100
|
+
interface RoutingRule {
|
|
101
|
+
id: string;
|
|
102
|
+
/** The address or pattern this rule matches. */
|
|
103
|
+
match: string;
|
|
104
|
+
/** `exact` for an address, `catch_all` for the domain's fallback. */
|
|
105
|
+
match_type: string;
|
|
106
|
+
/** `identity` for an inbox, `webhook` for a URL. */
|
|
107
|
+
destination_type: string;
|
|
108
|
+
destination: string;
|
|
109
|
+
status: string;
|
|
110
|
+
domain: string;
|
|
111
|
+
created_at: string;
|
|
112
|
+
}
|
|
113
|
+
interface Domain {
|
|
114
|
+
id: string;
|
|
115
|
+
domain: string;
|
|
116
|
+
status: string;
|
|
117
|
+
catch_all_enabled: boolean;
|
|
118
|
+
identity_count: number;
|
|
119
|
+
verified_at: string | null;
|
|
120
|
+
created_at: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A page of results.
|
|
124
|
+
*
|
|
125
|
+
* Deliberately not an async iterator. Hiding pagination hides the rate
|
|
126
|
+
* limit that pagination is subject to, and the application calling us is
|
|
127
|
+
* better placed to decide what to do when it is hit.
|
|
128
|
+
*/
|
|
129
|
+
interface Page<T> {
|
|
130
|
+
data: T[];
|
|
131
|
+
has_more: boolean;
|
|
132
|
+
next_cursor: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The Pidgeon client.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately thin: typed wrappers over `fetch`, one error class, and
|
|
139
|
+
* nothing else. No retry policy, no queue, no caching, no ambient state —
|
|
140
|
+
* those are decisions that belong to the application, and an SDK that makes
|
|
141
|
+
* them for you is an SDK you eventually have to work around.
|
|
142
|
+
*
|
|
143
|
+
* Zero dependencies. The only runtime requirements are `fetch` and
|
|
144
|
+
* `crypto.subtle`, both standard in Node 18+, Bun, Deno and every edge
|
|
145
|
+
* runtime.
|
|
146
|
+
*/
|
|
147
|
+
interface PidgeonOptions {
|
|
148
|
+
apiKey: string;
|
|
149
|
+
/** Defaults to the hosted API. Point this at a local dev server to test. */
|
|
150
|
+
baseUrl?: string;
|
|
151
|
+
/** Injected in tests. Defaults to the global. */
|
|
152
|
+
fetch?: typeof globalThis.fetch;
|
|
153
|
+
}
|
|
154
|
+
declare class PidgeonError extends Error {
|
|
155
|
+
readonly code: string;
|
|
156
|
+
readonly status: number;
|
|
157
|
+
readonly field?: string;
|
|
158
|
+
readonly requestId?: string;
|
|
159
|
+
constructor(input: {
|
|
160
|
+
code: string;
|
|
161
|
+
message: string;
|
|
162
|
+
status: number;
|
|
163
|
+
field?: string;
|
|
164
|
+
requestId?: string;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
declare class Pidgeon {
|
|
168
|
+
#private;
|
|
169
|
+
constructor(options: PidgeonOptions);
|
|
170
|
+
identities: {
|
|
171
|
+
list: () => Promise<Identity[]>;
|
|
172
|
+
get: (id: string) => Promise<Identity>;
|
|
173
|
+
create: (input: {
|
|
174
|
+
domain_id: string;
|
|
175
|
+
local_part: string;
|
|
176
|
+
display_name?: string | null;
|
|
177
|
+
type?: "human" | "team" | "application" | "workflow" | "agent";
|
|
178
|
+
mode?: "human" | "automated" | "hybrid";
|
|
179
|
+
}) => Promise<Identity>;
|
|
180
|
+
update: (id: string, input: {
|
|
181
|
+
display_name?: string | null;
|
|
182
|
+
mode?: "human" | "automated" | "hybrid";
|
|
183
|
+
capabilities?: Record<string, boolean>;
|
|
184
|
+
status?: "active" | "suspended" | "archived";
|
|
185
|
+
}) => Promise<Identity>;
|
|
186
|
+
delete: (id: string) => Promise<{
|
|
187
|
+
id: string;
|
|
188
|
+
}>;
|
|
189
|
+
};
|
|
190
|
+
messages: {
|
|
191
|
+
get: (id: string) => Promise<Message>;
|
|
192
|
+
listInThread: (threadId: string, options?: {
|
|
193
|
+
limit?: number;
|
|
194
|
+
}) => Promise<Page<Message>>;
|
|
195
|
+
send: (input: {
|
|
196
|
+
from: string;
|
|
197
|
+
to: string | string[];
|
|
198
|
+
cc?: string | string[];
|
|
199
|
+
bcc?: string | string[];
|
|
200
|
+
subject?: string;
|
|
201
|
+
text?: string;
|
|
202
|
+
in_reply_to?: string | null;
|
|
203
|
+
}) => Promise<{
|
|
204
|
+
id: string;
|
|
205
|
+
thread_id: string;
|
|
206
|
+
rejected: {
|
|
207
|
+
address: string;
|
|
208
|
+
}[];
|
|
209
|
+
}>;
|
|
210
|
+
};
|
|
211
|
+
attachments: {
|
|
212
|
+
/** A short-lived signed URL. Fetch it when you need it, don't store it. */
|
|
213
|
+
downloadUrl: (id: string) => Promise<{
|
|
214
|
+
id: string;
|
|
215
|
+
url: string;
|
|
216
|
+
}>;
|
|
217
|
+
};
|
|
218
|
+
threads: {
|
|
219
|
+
list: (options?: {
|
|
220
|
+
identity_id?: string;
|
|
221
|
+
status?: string;
|
|
222
|
+
limit?: number;
|
|
223
|
+
cursor?: string;
|
|
224
|
+
}) => Promise<Page<Thread>>;
|
|
225
|
+
get: (id: string) => Promise<Thread>;
|
|
226
|
+
update: (id: string, input: {
|
|
227
|
+
status?: "open" | "waiting" | "resolved" | "human_review";
|
|
228
|
+
owner_type?: "human" | "automation" | "agent";
|
|
229
|
+
}) => Promise<Thread>;
|
|
230
|
+
/** Hand the conversation to a person. The takeover primitive, named. */
|
|
231
|
+
handOff: (id: string) => Promise<Thread>;
|
|
232
|
+
};
|
|
233
|
+
webhooks: {
|
|
234
|
+
list: () => Promise<Webhook[]>;
|
|
235
|
+
/** The response carries `secret`, once. Store it before you discard it. */
|
|
236
|
+
create: (input: {
|
|
237
|
+
url: string;
|
|
238
|
+
identity_id?: string | null;
|
|
239
|
+
event_types?: string[];
|
|
240
|
+
}) => Promise<Webhook & {
|
|
241
|
+
secret: string;
|
|
242
|
+
}>;
|
|
243
|
+
delete: (id: string) => Promise<{
|
|
244
|
+
id: string;
|
|
245
|
+
}>;
|
|
246
|
+
};
|
|
247
|
+
events: {
|
|
248
|
+
list: (options?: {
|
|
249
|
+
type?: string;
|
|
250
|
+
identity_id?: string;
|
|
251
|
+
limit?: number;
|
|
252
|
+
cursor?: string;
|
|
253
|
+
}) => Promise<Page<Event>>;
|
|
254
|
+
get: (id: string) => Promise<Event>;
|
|
255
|
+
};
|
|
256
|
+
domains: {
|
|
257
|
+
/** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
|
|
258
|
+
list: () => Promise<Domain[]>;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verifying a delivery.
|
|
264
|
+
*
|
|
265
|
+
* The only part of this package with a security contract: it is what a
|
|
266
|
+
* customer runs to decide whether a request claiming a message arrived
|
|
267
|
+
* actually came from us. Everything else here is a typed `fetch`.
|
|
268
|
+
*
|
|
269
|
+
* Two properties carry that contract, and both are tested against payloads
|
|
270
|
+
* signed by the server's own implementation — see
|
|
271
|
+
* `tests/unit/sdk-webhook-verifier.test.ts`. Testing this file against
|
|
272
|
+
* itself would prove only that it agrees with itself, which is exactly the
|
|
273
|
+
* bug two implementations of one scheme produce.
|
|
274
|
+
*/
|
|
275
|
+
declare const SIGNATURE_HEADER = "x-pidgeon-signature";
|
|
276
|
+
declare const TIMESTAMP_HEADER = "x-pidgeon-timestamp";
|
|
277
|
+
interface WebhookEnvelope {
|
|
278
|
+
id: string;
|
|
279
|
+
type: string;
|
|
280
|
+
created_at: string;
|
|
281
|
+
data: Record<string, unknown>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Verifies a delivery and returns its payload.
|
|
285
|
+
*
|
|
286
|
+
* Throws rather than returning false, because the one thing this must not
|
|
287
|
+
* allow is a caller who forgets to check the boolean. Uses `crypto.subtle`,
|
|
288
|
+
* so it works unchanged in Node, Bun, Deno, Workers and Vercel functions.
|
|
289
|
+
*
|
|
290
|
+
* `rawBody` must be the exact bytes we sent. Re-serialising parsed JSON
|
|
291
|
+
* changes key order and whitespace, and the signature will not match — read
|
|
292
|
+
* the body as text before you parse it.
|
|
293
|
+
*/
|
|
294
|
+
declare function verifyWebhook(input: {
|
|
295
|
+
rawBody: string;
|
|
296
|
+
headers: Headers | Record<string, string | undefined>;
|
|
297
|
+
secret: string;
|
|
298
|
+
toleranceSeconds?: number;
|
|
299
|
+
now?: Date;
|
|
300
|
+
}): Promise<WebhookEnvelope>;
|
|
301
|
+
|
|
302
|
+
export { type Address, type Attachment, type Domain, type Event, type Identity, type Message, type Page, Pidgeon, PidgeonError, type PidgeonOptions, type RoutingRule, SIGNATURE_HEADER, TIMESTAMP_HEADER, type Thread, type Webhook, type WebhookEnvelope, verifyWebhook };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var PidgeonError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
field;
|
|
6
|
+
requestId;
|
|
7
|
+
constructor(input) {
|
|
8
|
+
super(input.message);
|
|
9
|
+
this.name = "PidgeonError";
|
|
10
|
+
this.code = input.code;
|
|
11
|
+
this.status = input.status;
|
|
12
|
+
this.field = input.field;
|
|
13
|
+
this.requestId = input.requestId;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var DEFAULT_BASE_URL = "https://pidgeon.ai/api/v1";
|
|
17
|
+
var Pidgeon = class {
|
|
18
|
+
#apiKey;
|
|
19
|
+
#baseUrl;
|
|
20
|
+
#fetch;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
if (!options.apiKey) throw new Error("An API key is required.");
|
|
23
|
+
this.#apiKey = options.apiKey;
|
|
24
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
25
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
26
|
+
}
|
|
27
|
+
async #request(method, path, options = {}) {
|
|
28
|
+
const url = new URL(`${this.#baseUrl}${path}`);
|
|
29
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
30
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
const response = await this.#fetch(url, {
|
|
33
|
+
method,
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
36
|
+
...options.body ? { "Content-Type": "application/json" } : {}
|
|
37
|
+
},
|
|
38
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
39
|
+
});
|
|
40
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
41
|
+
const payload = await response.json().catch(() => null);
|
|
42
|
+
if (!response.ok || payload?.error) {
|
|
43
|
+
throw new PidgeonError({
|
|
44
|
+
code: payload?.error?.code ?? "internal",
|
|
45
|
+
message: payload?.error?.message ?? `Request failed with ${response.status}.`,
|
|
46
|
+
status: response.status,
|
|
47
|
+
field: payload?.error?.field,
|
|
48
|
+
requestId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return payload?.data;
|
|
52
|
+
}
|
|
53
|
+
// --- identities ---------------------------------------------------------
|
|
54
|
+
identities = {
|
|
55
|
+
list: () => this.#request("GET", "/identities"),
|
|
56
|
+
get: (id) => this.#request("GET", `/identities/${id}`),
|
|
57
|
+
create: (input) => this.#request("POST", "/identities", { body: input }),
|
|
58
|
+
update: (id, input) => this.#request("PATCH", `/identities/${id}`, { body: input }),
|
|
59
|
+
delete: (id) => this.#request("DELETE", `/identities/${id}`)
|
|
60
|
+
};
|
|
61
|
+
// --- messages -----------------------------------------------------------
|
|
62
|
+
messages = {
|
|
63
|
+
get: (id) => this.#request("GET", `/messages/${id}`),
|
|
64
|
+
listInThread: (threadId, options = {}) => this.#requestPage("GET", "/messages", {
|
|
65
|
+
thread_id: threadId,
|
|
66
|
+
limit: options.limit
|
|
67
|
+
}),
|
|
68
|
+
send: (input) => this.#request(
|
|
69
|
+
"POST",
|
|
70
|
+
"/messages",
|
|
71
|
+
{ body: input }
|
|
72
|
+
)
|
|
73
|
+
};
|
|
74
|
+
attachments = {
|
|
75
|
+
/** A short-lived signed URL. Fetch it when you need it, don't store it. */
|
|
76
|
+
downloadUrl: (id) => this.#request("GET", `/attachments/${id}`)
|
|
77
|
+
};
|
|
78
|
+
// --- threads ------------------------------------------------------------
|
|
79
|
+
threads = {
|
|
80
|
+
list: (options = {}) => this.#requestPage("GET", "/threads", options),
|
|
81
|
+
get: (id) => this.#request("GET", `/threads/${id}`),
|
|
82
|
+
update: (id, input) => this.#request("PATCH", `/threads/${id}`, { body: input }),
|
|
83
|
+
/** Hand the conversation to a person. The takeover primitive, named. */
|
|
84
|
+
handOff: (id) => this.#request("PATCH", `/threads/${id}`, {
|
|
85
|
+
body: { owner_type: "human", status: "human_review" }
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
// --- webhooks -----------------------------------------------------------
|
|
89
|
+
webhooks = {
|
|
90
|
+
list: () => this.#request("GET", "/webhooks"),
|
|
91
|
+
/** The response carries `secret`, once. Store it before you discard it. */
|
|
92
|
+
create: (input) => this.#request("POST", "/webhooks", { body: input }),
|
|
93
|
+
delete: (id) => this.#request("DELETE", `/webhooks/${id}`)
|
|
94
|
+
};
|
|
95
|
+
// --- events -------------------------------------------------------------
|
|
96
|
+
events = {
|
|
97
|
+
list: (options = {}) => this.#requestPage("GET", "/events", options),
|
|
98
|
+
get: (id) => this.#request("GET", `/events/${id}`)
|
|
99
|
+
};
|
|
100
|
+
domains = {
|
|
101
|
+
/** Read-only. Adding and verifying a domain is a DNS-and-a-human task. */
|
|
102
|
+
list: () => this.#request("GET", "/domains")
|
|
103
|
+
};
|
|
104
|
+
async #requestPage(method, path, query) {
|
|
105
|
+
const url = new URL(`${this.#baseUrl}${path}`);
|
|
106
|
+
for (const [key, value] of Object.entries(query)) {
|
|
107
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
108
|
+
}
|
|
109
|
+
const response = await this.#fetch(url, {
|
|
110
|
+
method,
|
|
111
|
+
headers: { Authorization: `Bearer ${this.#apiKey}` }
|
|
112
|
+
});
|
|
113
|
+
const payload = await response.json().catch(() => null);
|
|
114
|
+
if (!response.ok || payload?.error) {
|
|
115
|
+
throw new PidgeonError({
|
|
116
|
+
code: payload?.error?.code ?? "internal",
|
|
117
|
+
message: payload?.error?.message ?? `Request failed with ${response.status}.`,
|
|
118
|
+
status: response.status,
|
|
119
|
+
field: payload?.error?.field,
|
|
120
|
+
requestId: response.headers.get("x-request-id") ?? void 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
data: payload?.data ?? [],
|
|
125
|
+
has_more: payload?.has_more ?? false,
|
|
126
|
+
next_cursor: payload?.next_cursor ?? null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/webhooks.ts
|
|
132
|
+
var SIGNATURE_HEADER = "x-pidgeon-signature";
|
|
133
|
+
var TIMESTAMP_HEADER = "x-pidgeon-timestamp";
|
|
134
|
+
async function verifyWebhook(input) {
|
|
135
|
+
const get = (name) => input.headers instanceof Headers ? input.headers.get(name) ?? void 0 : input.headers[name] ?? input.headers[name.toLowerCase()];
|
|
136
|
+
const timestamp = get(TIMESTAMP_HEADER);
|
|
137
|
+
const signature = get(SIGNATURE_HEADER);
|
|
138
|
+
if (!timestamp || !signature) throw new Error("This request is not signed.");
|
|
139
|
+
const seconds = Number(timestamp);
|
|
140
|
+
if (!Number.isFinite(seconds)) throw new Error("The signature timestamp is not a number.");
|
|
141
|
+
const now = Math.floor((input.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
142
|
+
const tolerance = input.toleranceSeconds ?? 300;
|
|
143
|
+
if (Math.abs(now - seconds) > tolerance) {
|
|
144
|
+
throw new Error("This request is too old to trust. It may be a replay.");
|
|
145
|
+
}
|
|
146
|
+
const key = await crypto.subtle.importKey(
|
|
147
|
+
"raw",
|
|
148
|
+
new TextEncoder().encode(input.secret),
|
|
149
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
150
|
+
false,
|
|
151
|
+
["sign"]
|
|
152
|
+
);
|
|
153
|
+
const mac = await crypto.subtle.sign(
|
|
154
|
+
"HMAC",
|
|
155
|
+
key,
|
|
156
|
+
new TextEncoder().encode(`${seconds}.${input.rawBody}`)
|
|
157
|
+
);
|
|
158
|
+
const expected = `v1=${[...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
159
|
+
if (!constantTimeEqual(expected, signature)) {
|
|
160
|
+
throw new Error("The signature does not match.");
|
|
161
|
+
}
|
|
162
|
+
return JSON.parse(input.rawBody);
|
|
163
|
+
}
|
|
164
|
+
function constantTimeEqual(a, b) {
|
|
165
|
+
if (a.length !== b.length) return false;
|
|
166
|
+
let difference = 0;
|
|
167
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
168
|
+
difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
169
|
+
}
|
|
170
|
+
return difference === 0;
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
Pidgeon,
|
|
174
|
+
PidgeonError,
|
|
175
|
+
SIGNATURE_HEADER,
|
|
176
|
+
TIMESTAMP_HEADER,
|
|
177
|
+
verifyWebhook
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/webhooks.ts"],"sourcesContent":["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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pidgeon-ai/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin TypeScript client for the Pidgeon email API.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pidgeon",
|
|
7
|
+
"email",
|
|
8
|
+
"webhooks",
|
|
9
|
+
"api-client"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/patrickcneuhaus/pidgeon.git",
|
|
15
|
+
"directory": "packages/sdk"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://pidgeon.ai",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.cjs",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"prepublishOnly": "pnpm build"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"tsup": "^8.5.0",
|
|
53
|
+
"typescript": "^5"
|
|
54
|
+
}
|
|
55
|
+
}
|