@pidgeon-ai/sdk 1.3.0 → 1.4.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 +64 -0
- package/dist/index.cjs +103 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -2
- package/dist/index.d.ts +112 -2
- package/dist/index.js +102 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,70 @@ call shapes, the result shape — not to the API behind it.
|
|
|
9
9
|
|
|
10
10
|
## Unreleased
|
|
11
11
|
|
|
12
|
+
## 1.4.0 — things the prose promised and the types did not have
|
|
13
|
+
|
|
14
|
+
**Additive, and two corrections to descriptions that were wrong.**
|
|
15
|
+
|
|
16
|
+
The rest of what a field test found on 15 October building a shared mailroom.
|
|
17
|
+
Everything below was either returned and undeclared, or described in the
|
|
18
|
+
documentation and absent from the product.
|
|
19
|
+
|
|
20
|
+
**Added to `Message`.**
|
|
21
|
+
|
|
22
|
+
- `headers`, which `skill.md` and the MCP `get_message` tool have both
|
|
23
|
+
promised for months — "its body **and its headers**" — against a schema
|
|
24
|
+
with no such property. It is a subset and the description says which:
|
|
25
|
+
`message-id`, `in-reply-to`, `references`, `date`, `reply-to`,
|
|
26
|
+
`return-path`, the four list and auto-reply headers, `x-mailer` and
|
|
27
|
+
`x-priority`. The whole envelope is `GET /messages/{id}/raw`. The
|
|
28
|
+
threading three are new to the kept set, because _will my reply land in
|
|
29
|
+
the right conversation_ is the question people open this for and nothing
|
|
30
|
+
answered it for mail between two Pidgeon addresses.
|
|
31
|
+
- `authentication` — `spf`, `dkim`, `dmarc`. `/docs/rules` has said since it
|
|
32
|
+
was written that if you need to be certain who sent something you should
|
|
33
|
+
_"read the authentication results on the message rather than its `From:`"_.
|
|
34
|
+
There were none in the contract: no such field anywhere. The columns have
|
|
35
|
+
been written since inbound mail existed. **Null means not reported, not
|
|
36
|
+
failed** — rendering an absent verdict as a failed one is worse than
|
|
37
|
+
showing nothing.
|
|
38
|
+
- `delivered_to` is the envelope recipient on every message. It was null
|
|
39
|
+
unless routing had moved the mail, while `message.received` carried it
|
|
40
|
+
every time, so the event and the read disagreed about the same message.
|
|
41
|
+
|
|
42
|
+
**Added to `Thread`.** `folder`, `from`, `from_name`, `preview` and
|
|
43
|
+
`has_attachments`, from the newest message. `GET /threads` is the only
|
|
44
|
+
workspace-wide listing and a row could say which conversations exist without
|
|
45
|
+
being able to draw one — so a merged inbox had to come from
|
|
46
|
+
`GET /mailboxes/{folder}`, which takes one address and one folder. Three
|
|
47
|
+
addresses and four folders is twelve calls for one screen, against a free
|
|
48
|
+
plan's 120 requests a minute. It is one call now.
|
|
49
|
+
|
|
50
|
+
**Added to `MailboxChange`.** `identity_id`, which was null on every row of
|
|
51
|
+
the only endpoint whose first required parameter is the mailbox.
|
|
52
|
+
|
|
53
|
+
**New error codes.** `blocked` and `not_permitted`, both `403`.
|
|
54
|
+
`/docs/rules`: _"a refusal tells you which kind it was… an agent debugging a
|
|
55
|
+
send needs to know which."_ Both answered `forbidden` and neither word
|
|
56
|
+
appeared in the response, so the only way to tell was to match the sentence.
|
|
57
|
+
The fix is different for each — remove a rule, or change the send policy —
|
|
58
|
+
which is the whole reason to separate them.
|
|
59
|
+
|
|
60
|
+
**Corrected.** Two descriptions that described something the API does not do:
|
|
61
|
+
|
|
62
|
+
- `rejected` on a send is **what the provider would not accept at hand-off**,
|
|
63
|
+
not a recipient your own rules forbid. That refuses the whole send with
|
|
64
|
+
`403`, and nothing goes to anybody. The old sentence told callers to handle
|
|
65
|
+
a partial send that never happens, and not to expect the exception they
|
|
66
|
+
actually get.
|
|
67
|
+
- `is_read` on `PATCH /messages/{id}`, and `read`/`unread` on
|
|
68
|
+
`POST /messages/actions`, are declared refused for an API key. They always
|
|
69
|
+
were — read state belongs to a person — but three surfaces advertised them
|
|
70
|
+
with no hint, including the MCP tool, whose description argued for a
|
|
71
|
+
capability it does not have to the one consumer that cannot check first.
|
|
72
|
+
|
|
73
|
+
**Widened.** `colour` on a label now has to be one of the eight; the
|
|
74
|
+
refusal's sentence is unchanged.
|
|
75
|
+
|
|
12
76
|
## 1.3.0 — a day of API that never reached a registry
|
|
13
77
|
|
|
14
78
|
**Additive, and one fix that was a blocker.**
|
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,7 @@ __export(index_exports, {
|
|
|
109
109
|
routingDelete: () => routingDelete,
|
|
110
110
|
routingList: () => routingList,
|
|
111
111
|
routingUpdate: () => routingUpdate,
|
|
112
|
+
streamEvents: () => streamEvents,
|
|
112
113
|
threadsGet: () => threadsGet,
|
|
113
114
|
threadsLabelsCreate: () => threadsLabelsCreate,
|
|
114
115
|
threadsLabelsDelete: () => threadsLabelsDelete,
|
|
@@ -175,7 +176,7 @@ function createSseClient({
|
|
|
175
176
|
...options
|
|
176
177
|
}) {
|
|
177
178
|
let lastEventId;
|
|
178
|
-
const
|
|
179
|
+
const sleep2 = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
179
180
|
const createStream = async function* () {
|
|
180
181
|
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
181
182
|
let attempt = 0;
|
|
@@ -279,7 +280,7 @@ function createSseClient({
|
|
|
279
280
|
break;
|
|
280
281
|
}
|
|
281
282
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
|
|
282
|
-
await
|
|
283
|
+
await sleep2(backoff);
|
|
283
284
|
}
|
|
284
285
|
}
|
|
285
286
|
};
|
|
@@ -1647,6 +1648,105 @@ function constantTimeEqual(a, b) {
|
|
|
1647
1648
|
}
|
|
1648
1649
|
return difference === 0;
|
|
1649
1650
|
}
|
|
1651
|
+
|
|
1652
|
+
// src/stream.ts
|
|
1653
|
+
async function* streamEvents(options) {
|
|
1654
|
+
const base = (options.baseUrl ?? "https://pidgeon.ai/api/v1").replace(/\/$/, "");
|
|
1655
|
+
let lastEventId;
|
|
1656
|
+
let backoffMs = 1e3;
|
|
1657
|
+
while (!options.signal?.aborted) {
|
|
1658
|
+
const url = new URL(`${base}/events/stream`);
|
|
1659
|
+
if (options.identityId) url.searchParams.set("identity_id", options.identityId);
|
|
1660
|
+
if (options.type) url.searchParams.set("type", options.type);
|
|
1661
|
+
if (options.after && !lastEventId) url.searchParams.set("after", options.after);
|
|
1662
|
+
let response;
|
|
1663
|
+
try {
|
|
1664
|
+
response = await fetch(url, {
|
|
1665
|
+
headers: {
|
|
1666
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
1667
|
+
accept: "text/event-stream",
|
|
1668
|
+
...lastEventId ? { "last-event-id": lastEventId } : {}
|
|
1669
|
+
},
|
|
1670
|
+
signal: options.signal
|
|
1671
|
+
});
|
|
1672
|
+
} catch {
|
|
1673
|
+
if (options.signal?.aborted) return;
|
|
1674
|
+
await sleep(backoffMs, options.signal);
|
|
1675
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
`Pidgeon refused the stream: ${response.status} ${await response.text().catch(() => "")}`.trim()
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (!response.ok || !response.body) {
|
|
1684
|
+
await sleep(backoffMs, options.signal);
|
|
1685
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
backoffMs = 1e3;
|
|
1689
|
+
for await (const frame of frames(response.body, options.signal)) {
|
|
1690
|
+
if (frame.id) lastEventId = frame.id;
|
|
1691
|
+
if (!frame.data) continue;
|
|
1692
|
+
if (frame.event === "error") continue;
|
|
1693
|
+
let parsed;
|
|
1694
|
+
try {
|
|
1695
|
+
parsed = JSON.parse(frame.data);
|
|
1696
|
+
} catch {
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
yield parsed;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
async function* frames(body, signal) {
|
|
1704
|
+
const reader = body.getReader();
|
|
1705
|
+
const decoder = new TextDecoder();
|
|
1706
|
+
let buffer = "";
|
|
1707
|
+
try {
|
|
1708
|
+
while (!signal?.aborted) {
|
|
1709
|
+
const { done, value } = await reader.read();
|
|
1710
|
+
if (done) return;
|
|
1711
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1712
|
+
let boundary = buffer.indexOf("\n\n");
|
|
1713
|
+
while (boundary !== -1) {
|
|
1714
|
+
const block = buffer.slice(0, boundary);
|
|
1715
|
+
buffer = buffer.slice(boundary + 2);
|
|
1716
|
+
boundary = buffer.indexOf("\n\n");
|
|
1717
|
+
const frame = {};
|
|
1718
|
+
const data = [];
|
|
1719
|
+
for (const line of block.split("\n")) {
|
|
1720
|
+
if (line.startsWith(":")) continue;
|
|
1721
|
+
const colon = line.indexOf(":");
|
|
1722
|
+
if (colon === -1) continue;
|
|
1723
|
+
const field = line.slice(0, colon);
|
|
1724
|
+
const value2 = line.slice(colon + 1).replace(/^ /, "");
|
|
1725
|
+
if (field === "id") frame.id = value2;
|
|
1726
|
+
else if (field === "event") frame.event = value2;
|
|
1727
|
+
else if (field === "data") data.push(value2);
|
|
1728
|
+
}
|
|
1729
|
+
if (data.length > 0) frame.data = data.join("\n");
|
|
1730
|
+
if (frame.id || frame.data) yield frame;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} finally {
|
|
1734
|
+
await reader.cancel().catch(() => void 0);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function sleep(ms, signal) {
|
|
1738
|
+
return new Promise((resolve) => {
|
|
1739
|
+
const timer = setTimeout(resolve, ms);
|
|
1740
|
+
signal?.addEventListener(
|
|
1741
|
+
"abort",
|
|
1742
|
+
() => {
|
|
1743
|
+
clearTimeout(timer);
|
|
1744
|
+
resolve();
|
|
1745
|
+
},
|
|
1746
|
+
{ once: true }
|
|
1747
|
+
);
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1650
1750
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1651
1751
|
0 && (module.exports = {
|
|
1652
1752
|
SIGNATURE_HEADER,
|
|
@@ -1738,6 +1838,7 @@ function constantTimeEqual(a, b) {
|
|
|
1738
1838
|
routingDelete,
|
|
1739
1839
|
routingList,
|
|
1740
1840
|
routingUpdate,
|
|
1841
|
+
streamEvents,
|
|
1741
1842
|
threadsGet,
|
|
1742
1843
|
threadsLabelsCreate,
|
|
1743
1844
|
threadsLabelsDelete,
|