@pidgeon-ai/sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -3
- package/README.md +76 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -5
- package/dist/index.d.ts +44 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
Versioned independently of the application. The API is `/v1`; this package
|
|
4
|
-
is `
|
|
4
|
+
is `1.x`. They are different contracts with different audiences, and tying
|
|
5
5
|
the two would publish a release for every unrelated product deploy.
|
|
6
6
|
|
|
7
|
-
Semver applies to **this package's own surface** — the exported
|
|
8
|
-
|
|
7
|
+
Semver applies to **this package's own surface** — the exported names, the
|
|
8
|
+
call shapes, the result shape — not to the API behind it.
|
|
9
9
|
|
|
10
10
|
## Unreleased
|
|
11
11
|
|
|
12
|
+
## 1.1.0 — the domain an address is on
|
|
13
|
+
|
|
14
|
+
Additive. Nothing here changes a call that already works.
|
|
15
|
+
|
|
16
|
+
**Added**
|
|
17
|
+
|
|
18
|
+
- `Identity.domain_id`. Every address now says which domain it is on, not
|
|
19
|
+
only its name. This exists because the id was otherwise unlearnable: an
|
|
20
|
+
account whose only domain is the shared one gets `[]` from `GET /domains`,
|
|
21
|
+
correctly — that endpoint lists what a workspace _owns_ — and `domain_id`
|
|
22
|
+
is what `identitiesCreate` requires. A customer building against production
|
|
23
|
+
read the uuid out of the database to get past it.
|
|
24
|
+
- `identitiesCreate` takes `domain` (a name) or `address` (a whole one) in
|
|
25
|
+
place of `domain_id`. Pass exactly one. `address` also supplies the local
|
|
26
|
+
part, which is the form `skills/pidgeon/SKILL.md` has documented since it
|
|
27
|
+
was written.
|
|
28
|
+
- `threadsList` takes `identity_id`, `status`, `owner_type` and
|
|
29
|
+
`assignee_id`. The API has honoured all four since it was written and the
|
|
30
|
+
contract declared none of them, so no generated client could offer them.
|
|
31
|
+
`assignee_id` takes a person's id or the literal `unassigned`.
|
|
32
|
+
|
|
33
|
+
## 1.0.0 — one name per operation, generated
|
|
34
|
+
|
|
35
|
+
**Published without an entry here**, which is how a changelog stops being
|
|
36
|
+
read. Recorded after the fact, on 13 October, when a customer found the
|
|
37
|
+
package README describing a class the package does not export.
|
|
38
|
+
|
|
39
|
+
**Breaking.** The hand-written client is gone. There is no `Pidgeon` class
|
|
40
|
+
and no `PidgeonError`: the package is generated from `openapi.json`, one
|
|
41
|
+
function per operation named for its tag — `messagesSend`, `messagesGet`,
|
|
42
|
+
`threadsUpdate` — sharing one client you configure at startup with
|
|
43
|
+
`client.setConfig`. A call resolves to `{ data, error }` rather than
|
|
44
|
+
throwing, or pass `throwOnError: true`.
|
|
45
|
+
|
|
46
|
+
Both clients are generated from the same document, so an operation has the
|
|
47
|
+
same name in TypeScript and in Python. `decisions.md` §343 is the argument,
|
|
48
|
+
including why the stutter in `messagesSend` is accepted.
|
|
49
|
+
|
|
12
50
|
## 0.2.0 — the mailbox, and the credentials to reach it
|
|
13
51
|
|
|
14
52
|
A minor rather than a patch, because one signature changed. In `0.x` semver
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @pidgeon-ai/sdk
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
A TypeScript client for the [Pidgeon](https://pidgeon.ai) email API,
|
|
4
|
+
generated from `openapi.json`: one typed function per operation, plus a
|
|
5
|
+
webhook verifier.
|
|
5
6
|
|
|
6
7
|
**Zero dependencies.** Needs `fetch` and `crypto.subtle`, so it runs on
|
|
7
8
|
Node 18+, Bun, Deno, Cloudflare Workers and Vercel functions. ESM and
|
|
@@ -11,15 +12,35 @@ CommonJS.
|
|
|
11
12
|
pnpm add @pidgeon-ai/sdk
|
|
12
13
|
```
|
|
13
14
|
|
|
15
|
+
## The shape
|
|
16
|
+
|
|
17
|
+
**There is no `Pidgeon` class.** Every operation is a function named for its
|
|
18
|
+
tag — `messagesSend`, `messagesGet`, `threadsUpdate` — and they share one
|
|
19
|
+
client you configure at startup. The Python package is generated from the same
|
|
20
|
+
document, so an operation has the same name in both.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { client, messagesGet } from '@pidgeon-ai/sdk';
|
|
24
|
+
|
|
25
|
+
client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });
|
|
26
|
+
|
|
27
|
+
const { data, error } = await messagesGet({ path: { id: messageId } });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A call resolves to `{ data, error }` rather than throwing: `error` carries the
|
|
31
|
+
API's own `code`, a `message` written for a person, and a `field` when one
|
|
32
|
+
input was at fault. Pass `throwOnError: true` on a call to get the data
|
|
33
|
+
directly and an exception instead.
|
|
34
|
+
|
|
14
35
|
## Receiving mail
|
|
15
36
|
|
|
16
37
|
Mail arriving at an address your code owns becomes an event, and your
|
|
17
38
|
endpoint is called with a signed payload.
|
|
18
39
|
|
|
19
40
|
```ts
|
|
20
|
-
import {
|
|
41
|
+
import { client, messagesGet, messagesSend, verifyWebhook } from '@pidgeon-ai/sdk';
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });
|
|
23
44
|
|
|
24
45
|
export async function POST(request: Request) {
|
|
25
46
|
// Verify first. An unverified payload is an unauthenticated POST that
|
|
@@ -33,15 +54,21 @@ export async function POST(request: Request) {
|
|
|
33
54
|
if (event.type !== 'message.received') return new Response(null, { status: 204 });
|
|
34
55
|
|
|
35
56
|
// The event carries ids, never a body. Exchange one for the content.
|
|
36
|
-
const mail = await
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
const { data: mail } = await messagesGet({
|
|
58
|
+
path: { id: event.data.message_id as string },
|
|
59
|
+
throwOnError: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await messagesSend({
|
|
63
|
+
body: {
|
|
64
|
+
from: mail.data.to[0]!.email,
|
|
65
|
+
to: mail.data.from.email,
|
|
66
|
+
subject: `Re: ${mail.data.subject}`,
|
|
67
|
+
text: 'Got it — someone will be in touch.',
|
|
68
|
+
// Keeps the conversation together in the recipient's client too.
|
|
69
|
+
in_reply_to: mail.data.id,
|
|
70
|
+
},
|
|
71
|
+
throwOnError: true,
|
|
45
72
|
});
|
|
46
73
|
|
|
47
74
|
return new Response(null, { status: 204 });
|
|
@@ -60,45 +87,60 @@ the signature, and compares in constant time.
|
|
|
60
87
|
## Handing a conversation to a person
|
|
61
88
|
|
|
62
89
|
```ts
|
|
63
|
-
|
|
90
|
+
import { threadsUpdate } from '@pidgeon-ai/sdk';
|
|
91
|
+
|
|
92
|
+
await threadsUpdate({
|
|
93
|
+
path: { id: threadId },
|
|
94
|
+
body: { owner_type: 'human' },
|
|
95
|
+
throwOnError: true,
|
|
96
|
+
});
|
|
64
97
|
```
|
|
65
98
|
|
|
99
|
+
Hand it back by setting `owner_type` to `agent`.
|
|
100
|
+
|
|
66
101
|
It appears in the web inbox, and `thread.updated` tells whatever was
|
|
67
102
|
driving the conversation that it no longer is.
|
|
68
103
|
|
|
69
104
|
## Errors
|
|
70
105
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
106
|
+
A failure comes back in `error`, carrying the API's own `code`, a `message`
|
|
107
|
+
written for a person and safe to show one, and the offending `field` on a
|
|
108
|
+
validation failure.
|
|
74
109
|
|
|
75
110
|
```ts
|
|
76
|
-
|
|
111
|
+
const { data, error } = await messagesSend({ body: { from, to, text } });
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if (error instanceof PidgeonError && error.code === 'rate_limited') {
|
|
113
|
+
if (error) {
|
|
114
|
+
if (error.error.code === 'rate_limited') {
|
|
82
115
|
// Back off. Deliberately not done for you — see below.
|
|
83
116
|
}
|
|
84
|
-
throw error;
|
|
117
|
+
throw new Error(error.error.message);
|
|
85
118
|
}
|
|
86
119
|
```
|
|
87
120
|
|
|
88
121
|
`code` is one of `validation`, `unauthenticated`, `entitlement`,
|
|
89
122
|
`forbidden`, `not_found`, `conflict`, `rate_limited`, `internal`.
|
|
90
123
|
|
|
124
|
+
With `throwOnError: true` the same failure is thrown instead and `data` is no
|
|
125
|
+
longer `undefined`, which is the shape most call sites want.
|
|
126
|
+
|
|
91
127
|
## Pagination
|
|
92
128
|
|
|
93
129
|
List endpoints return a page, not an iterator.
|
|
94
130
|
|
|
95
131
|
```ts
|
|
96
|
-
|
|
132
|
+
import { eventsList } from '@pidgeon-ai/sdk';
|
|
133
|
+
|
|
134
|
+
let cursor: string | undefined;
|
|
97
135
|
|
|
98
136
|
do {
|
|
99
|
-
const page = await
|
|
137
|
+
const { data: page } = await eventsList({
|
|
138
|
+
query: { limit: 50, cursor },
|
|
139
|
+
throwOnError: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
100
142
|
for (const event of page.data) handle(event);
|
|
101
|
-
cursor = page.next_cursor;
|
|
143
|
+
cursor = page.next_cursor ?? undefined;
|
|
102
144
|
} while (cursor);
|
|
103
145
|
```
|
|
104
146
|
|
|
@@ -112,8 +154,10 @@ pagination also hides the rate limit pagination is subject to.
|
|
|
112
154
|
## Options
|
|
113
155
|
|
|
114
156
|
```ts
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
import { client } from '@pidgeon-ai/sdk';
|
|
158
|
+
|
|
159
|
+
client.setConfig({
|
|
160
|
+
auth: () => process.env.PIDGEON_API_KEY!,
|
|
117
161
|
// Point at a local dev server, or a preview deployment.
|
|
118
162
|
baseUrl: 'http://localhost:3000/api/v1',
|
|
119
163
|
// Inject a transport. Useful in tests.
|
|
@@ -121,10 +165,13 @@ new Pidgeon({
|
|
|
121
165
|
});
|
|
122
166
|
```
|
|
123
167
|
|
|
168
|
+
`createClient(createConfig({ … }))` makes a second, independent client, for
|
|
169
|
+
code that talks to two accounts; pass it as `client` on a call.
|
|
170
|
+
|
|
124
171
|
## Versioning
|
|
125
172
|
|
|
126
173
|
Versioned independently of the API. The API is `/v1`; this package is
|
|
127
|
-
`
|
|
174
|
+
`1.x`, and semver applies to its own surface — the exported types, the
|
|
128
175
|
method signatures, the error shape. See `CHANGELOG.md`.
|
|
129
176
|
|
|
130
177
|
MIT.
|