@pidgeon-ai/sdk 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,93 @@ call shapes, the result shape — not to the API behind it.
9
9
 
10
10
  ## Unreleased
11
11
 
12
+ ## 1.5.0 — a container your customers live in
13
+
14
+ **Additive.** Every account has exactly one pod already, every address is in
15
+ it, and a caller who never mentions a pod cannot tell this release from the
16
+ last one.
17
+
18
+ **Added.** `podsList`, `podsCreate`, `podsGet`, `podsUpdate`, the `Pod` type,
19
+ `pod_id` on an address, and `pod_id` as a filter on `identitiesList`.
20
+
21
+ A **pod** holds addresses and domains: one of your customers, a client, or
22
+ just test kept apart from production. It has a name and an optional
23
+ `client_id` that is your own id for it, carried and never parsed.
24
+
25
+ **It is organisational and not a boundary**, and the types say so because the
26
+ risk in shipping the container before the isolation is a reader assuming the
27
+ isolation. A pod carries no sending reputation of its own — one SES tenant
28
+ per _account_, unchanged — and **a key is still a list of address ids**,
29
+ never something scoped to a pod.
30
+
31
+ **No delete.** The verb that destroys a container full of mailboxes is worth
32
+ making somebody ask for, and nothing yet moves addresses between pods, so it
33
+ would either refuse every non-empty pod or destroy mail.
34
+
35
+ ## 1.4.0 — things the prose promised and the types did not have
36
+
37
+ **Additive, and two corrections to descriptions that were wrong.**
38
+
39
+ The rest of what a field test found on 15 October building a shared mailroom.
40
+ Everything below was either returned and undeclared, or described in the
41
+ documentation and absent from the product.
42
+
43
+ **Added to `Message`.**
44
+
45
+ - `headers`, which `skill.md` and the MCP `get_message` tool have both
46
+ promised for months — "its body **and its headers**" — against a schema
47
+ with no such property. It is a subset and the description says which:
48
+ `message-id`, `in-reply-to`, `references`, `date`, `reply-to`,
49
+ `return-path`, the four list and auto-reply headers, `x-mailer` and
50
+ `x-priority`. The whole envelope is `GET /messages/{id}/raw`. The
51
+ threading three are new to the kept set, because _will my reply land in
52
+ the right conversation_ is the question people open this for and nothing
53
+ answered it for mail between two Pidgeon addresses.
54
+ - `authentication` — `spf`, `dkim`, `dmarc`. `/docs/rules` has said since it
55
+ was written that if you need to be certain who sent something you should
56
+ _"read the authentication results on the message rather than its `From:`"_.
57
+ There were none in the contract: no such field anywhere. The columns have
58
+ been written since inbound mail existed. **Null means not reported, not
59
+ failed** — rendering an absent verdict as a failed one is worse than
60
+ showing nothing.
61
+ - `delivered_to` is the envelope recipient on every message. It was null
62
+ unless routing had moved the mail, while `message.received` carried it
63
+ every time, so the event and the read disagreed about the same message.
64
+
65
+ **Added to `Thread`.** `folder`, `from`, `from_name`, `preview` and
66
+ `has_attachments`, from the newest message. `GET /threads` is the only
67
+ workspace-wide listing and a row could say which conversations exist without
68
+ being able to draw one — so a merged inbox had to come from
69
+ `GET /mailboxes/{folder}`, which takes one address and one folder. Three
70
+ addresses and four folders is twelve calls for one screen, against a free
71
+ plan's 120 requests a minute. It is one call now.
72
+
73
+ **Added to `MailboxChange`.** `identity_id`, which was null on every row of
74
+ the only endpoint whose first required parameter is the mailbox.
75
+
76
+ **New error codes.** `blocked` and `not_permitted`, both `403`.
77
+ `/docs/rules`: _"a refusal tells you which kind it was… an agent debugging a
78
+ send needs to know which."_ Both answered `forbidden` and neither word
79
+ appeared in the response, so the only way to tell was to match the sentence.
80
+ The fix is different for each — remove a rule, or change the send policy —
81
+ which is the whole reason to separate them.
82
+
83
+ **Corrected.** Two descriptions that described something the API does not do:
84
+
85
+ - `rejected` on a send is **what the provider would not accept at hand-off**,
86
+ not a recipient your own rules forbid. That refuses the whole send with
87
+ `403`, and nothing goes to anybody. The old sentence told callers to handle
88
+ a partial send that never happens, and not to expect the exception they
89
+ actually get.
90
+ - `is_read` on `PATCH /messages/{id}`, and `read`/`unread` on
91
+ `POST /messages/actions`, are declared refused for an API key. They always
92
+ were — read state belongs to a person — but three surfaces advertised them
93
+ with no hint, including the MCP tool, whose description argued for a
94
+ capability it does not have to the one consumer that cannot check first.
95
+
96
+ **Widened.** `colour` on a label now has to be one of the eight; the
97
+ refusal's sentence is unchanged.
98
+
12
99
  ## 1.3.0 — a day of API that never reached a registry
13
100
 
14
101
  **Additive, and one fix that was a blocker.**
package/dist/index.cjs CHANGED
@@ -105,10 +105,15 @@ __export(index_exports, {
105
105
  messagesUndo: () => messagesUndo,
106
106
  messagesUpdate: () => messagesUpdate,
107
107
  metricsQuery: () => metricsQuery,
108
+ podsCreate: () => podsCreate,
109
+ podsGet: () => podsGet,
110
+ podsList: () => podsList,
111
+ podsUpdate: () => podsUpdate,
108
112
  routingCreate: () => routingCreate,
109
113
  routingDelete: () => routingDelete,
110
114
  routingList: () => routingList,
111
115
  routingUpdate: () => routingUpdate,
116
+ streamEvents: () => streamEvents,
112
117
  threadsGet: () => threadsGet,
113
118
  threadsLabelsCreate: () => threadsLabelsCreate,
114
119
  threadsLabelsDelete: () => threadsLabelsDelete,
@@ -175,7 +180,7 @@ function createSseClient({
175
180
  ...options
176
181
  }) {
177
182
  let lastEventId;
178
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
183
+ const sleep2 = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
179
184
  const createStream = async function* () {
180
185
  let retryDelay = sseDefaultRetryDelay ?? 3e3;
181
186
  let attempt = 0;
@@ -279,7 +284,7 @@ function createSseClient({
279
284
  break;
280
285
  }
281
286
  const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
282
- await sleep(backoff);
287
+ await sleep2(backoff);
283
288
  }
284
289
  }
285
290
  };
@@ -1058,6 +1063,34 @@ var identitiesSuspendTenant = (options) => (options.client ?? client).post({
1058
1063
  url: "/tenants/{id}/suspend",
1059
1064
  ...options
1060
1065
  });
1066
+ var podsList = (options) => (options?.client ?? client).get({
1067
+ security: [{ scheme: "bearer", type: "http" }],
1068
+ url: "/pods",
1069
+ ...options
1070
+ });
1071
+ var podsCreate = (options) => (options.client ?? client).post({
1072
+ security: [{ scheme: "bearer", type: "http" }],
1073
+ url: "/pods",
1074
+ ...options,
1075
+ headers: {
1076
+ "Content-Type": "application/json",
1077
+ ...options.headers
1078
+ }
1079
+ });
1080
+ var podsGet = (options) => (options.client ?? client).get({
1081
+ security: [{ scheme: "bearer", type: "http" }],
1082
+ url: "/pods/{id}",
1083
+ ...options
1084
+ });
1085
+ var podsUpdate = (options) => (options.client ?? client).patch({
1086
+ security: [{ scheme: "bearer", type: "http" }],
1087
+ url: "/pods/{id}",
1088
+ ...options,
1089
+ headers: {
1090
+ "Content-Type": "application/json",
1091
+ ...options.headers
1092
+ }
1093
+ });
1061
1094
  var labelsList = (options) => (options.client ?? client).get({
1062
1095
  security: [{ scheme: "bearer", type: "http" }],
1063
1096
  url: "/labels",
@@ -1647,6 +1680,105 @@ function constantTimeEqual(a, b) {
1647
1680
  }
1648
1681
  return difference === 0;
1649
1682
  }
1683
+
1684
+ // src/stream.ts
1685
+ async function* streamEvents(options) {
1686
+ const base = (options.baseUrl ?? "https://pidgeon.ai/api/v1").replace(/\/$/, "");
1687
+ let lastEventId;
1688
+ let backoffMs = 1e3;
1689
+ while (!options.signal?.aborted) {
1690
+ const url = new URL(`${base}/events/stream`);
1691
+ if (options.identityId) url.searchParams.set("identity_id", options.identityId);
1692
+ if (options.type) url.searchParams.set("type", options.type);
1693
+ if (options.after && !lastEventId) url.searchParams.set("after", options.after);
1694
+ let response;
1695
+ try {
1696
+ response = await fetch(url, {
1697
+ headers: {
1698
+ authorization: `Bearer ${options.apiKey}`,
1699
+ accept: "text/event-stream",
1700
+ ...lastEventId ? { "last-event-id": lastEventId } : {}
1701
+ },
1702
+ signal: options.signal
1703
+ });
1704
+ } catch {
1705
+ if (options.signal?.aborted) return;
1706
+ await sleep(backoffMs, options.signal);
1707
+ backoffMs = Math.min(backoffMs * 2, 3e4);
1708
+ continue;
1709
+ }
1710
+ if (response.status === 401 || response.status === 403 || response.status === 404) {
1711
+ throw new Error(
1712
+ `Pidgeon refused the stream: ${response.status} ${await response.text().catch(() => "")}`.trim()
1713
+ );
1714
+ }
1715
+ if (!response.ok || !response.body) {
1716
+ await sleep(backoffMs, options.signal);
1717
+ backoffMs = Math.min(backoffMs * 2, 3e4);
1718
+ continue;
1719
+ }
1720
+ backoffMs = 1e3;
1721
+ for await (const frame of frames(response.body, options.signal)) {
1722
+ if (frame.id) lastEventId = frame.id;
1723
+ if (!frame.data) continue;
1724
+ if (frame.event === "error") continue;
1725
+ let parsed;
1726
+ try {
1727
+ parsed = JSON.parse(frame.data);
1728
+ } catch {
1729
+ continue;
1730
+ }
1731
+ yield parsed;
1732
+ }
1733
+ }
1734
+ }
1735
+ async function* frames(body, signal) {
1736
+ const reader = body.getReader();
1737
+ const decoder = new TextDecoder();
1738
+ let buffer = "";
1739
+ try {
1740
+ while (!signal?.aborted) {
1741
+ const { done, value } = await reader.read();
1742
+ if (done) return;
1743
+ buffer += decoder.decode(value, { stream: true });
1744
+ let boundary = buffer.indexOf("\n\n");
1745
+ while (boundary !== -1) {
1746
+ const block = buffer.slice(0, boundary);
1747
+ buffer = buffer.slice(boundary + 2);
1748
+ boundary = buffer.indexOf("\n\n");
1749
+ const frame = {};
1750
+ const data = [];
1751
+ for (const line of block.split("\n")) {
1752
+ if (line.startsWith(":")) continue;
1753
+ const colon = line.indexOf(":");
1754
+ if (colon === -1) continue;
1755
+ const field = line.slice(0, colon);
1756
+ const value2 = line.slice(colon + 1).replace(/^ /, "");
1757
+ if (field === "id") frame.id = value2;
1758
+ else if (field === "event") frame.event = value2;
1759
+ else if (field === "data") data.push(value2);
1760
+ }
1761
+ if (data.length > 0) frame.data = data.join("\n");
1762
+ if (frame.id || frame.data) yield frame;
1763
+ }
1764
+ }
1765
+ } finally {
1766
+ await reader.cancel().catch(() => void 0);
1767
+ }
1768
+ }
1769
+ function sleep(ms, signal) {
1770
+ return new Promise((resolve) => {
1771
+ const timer = setTimeout(resolve, ms);
1772
+ signal?.addEventListener(
1773
+ "abort",
1774
+ () => {
1775
+ clearTimeout(timer);
1776
+ resolve();
1777
+ },
1778
+ { once: true }
1779
+ );
1780
+ });
1781
+ }
1650
1782
  // Annotate the CommonJS export names for ESM import in node:
1651
1783
  0 && (module.exports = {
1652
1784
  SIGNATURE_HEADER,
@@ -1734,10 +1866,15 @@ function constantTimeEqual(a, b) {
1734
1866
  messagesUndo,
1735
1867
  messagesUpdate,
1736
1868
  metricsQuery,
1869
+ podsCreate,
1870
+ podsGet,
1871
+ podsList,
1872
+ podsUpdate,
1737
1873
  routingCreate,
1738
1874
  routingDelete,
1739
1875
  routingList,
1740
1876
  routingUpdate,
1877
+ streamEvents,
1741
1878
  threadsGet,
1742
1879
  threadsLabelsCreate,
1743
1880
  threadsLabelsDelete,