@posthaste/sdk 0.1.0 → 0.3.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/README.md +182 -31
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +22 -0
- package/dist/client.d.ts +102 -11
- package/dist/client.js +181 -14
- package/dist/errors.d.ts +46 -4
- package/dist/errors.js +40 -4
- package/dist/http.d.ts +41 -5
- package/dist/http.js +71 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/pagination.d.ts +66 -24
- package/dist/pagination.js +58 -26
- package/dist/types.d.ts +224 -20
- package/dist/types.js +8 -2
- package/package.json +1 -1
package/dist/pagination.d.ts
CHANGED
|
@@ -1,40 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keyset pagination.
|
|
3
3
|
*
|
|
4
|
-
* The API pages by cursor rather than by offset
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
4
|
+
* The API pages by cursor rather than by offset. Most lists take `limit` +
|
|
5
|
+
* `before` and answer `{ data, hasMore, nextCursor }` or `{ data, nextCursor }`,
|
|
6
|
+
* where `before` is the id of the last row you received — which is what
|
|
7
|
+
* `nextCursor` hands you.
|
|
8
|
+
*
|
|
9
|
+
* THE TWO TRAPS THIS MODULE EXISTS TO CLOSE
|
|
10
|
+
*
|
|
11
|
+
* They are mirror images, which is why neither `hasMore` nor `nextCursor` alone
|
|
12
|
+
* is a correct stopping rule across the whole API:
|
|
13
|
+
*
|
|
14
|
+
* 1. `while (nextCursor)` spins for ever on `/v1/inbound/messages`. Its cursor
|
|
15
|
+
* is derived from the last row of the page and is set whenever the page has
|
|
16
|
+
* any rows at all, including on the final one. A loop over it asks for the
|
|
17
|
+
* page after the last, gets an empty page carrying the same cursor, and
|
|
18
|
+
* never finishes. Nothing about it looks wrong in a log.
|
|
19
|
+
*
|
|
20
|
+
* 2. `if (!page.hasMore) return` stops after ONE page on `/v1/domains`,
|
|
21
|
+
* `/v1/webhooks`, `/v1/api-keys`, `/v1/inbound/addresses`,
|
|
22
|
+
* `/v1/billing/invoices` and `/v1/billing/history`. Those endpoints send no
|
|
23
|
+
* `hasMore` at all, so the read is `undefined`, which is falsy, which looks
|
|
24
|
+
* exactly like "that was everything". This is the failure that shipped: an
|
|
25
|
+
* account with sixty invoices got fifty of them and no indication that ten
|
|
26
|
+
* were missing.
|
|
27
|
+
*
|
|
28
|
+
* So the walker consults BOTH, and treats each as authoritative only where the
|
|
29
|
+
* server actually sends it: stop when `hasMore` is explicitly `false`, and stop
|
|
30
|
+
* when the cursor is absent or fails to advance. Every endpoint is covered by at
|
|
31
|
+
* least one of those, and no endpoint is stopped early by either.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The least a response has to be for the walker to walk it.
|
|
35
|
+
*
|
|
36
|
+
* `hasMore` is optional here on purpose — that is the actual contract, and
|
|
37
|
+
* making it optional in the type is what forces every read of it to be an
|
|
38
|
+
* explicit `=== false` rather than a truthiness test.
|
|
39
|
+
*/
|
|
40
|
+
export interface WalkablePage<T> {
|
|
41
|
+
data: T[];
|
|
42
|
+
nextCursor: string | null;
|
|
43
|
+
hasMore?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export type PageFetcher<TItem, TParams> = (params: TParams) => Promise<WalkablePage<TItem>>;
|
|
46
|
+
/**
|
|
47
|
+
* Which request parameter carries the cursor.
|
|
48
|
+
*
|
|
49
|
+
* `before` everywhere except `/v1/billing/history`, which reads forward from a
|
|
50
|
+
* sequence number in `after`.
|
|
51
|
+
*/
|
|
52
|
+
export type CursorParam = 'before' | 'after';
|
|
53
|
+
/**
|
|
54
|
+
* The cursor-bearing shape of a list's request parameters.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately not `PaginationParams`: `/v1/billing/history` has no `before` at
|
|
57
|
+
* all, so a constraint written in terms of it would exclude the one endpoint
|
|
58
|
+
* whose cursor convention most needs a helper. Every field is optional, so any
|
|
59
|
+
* of the SDK's `List*Params` interfaces satisfies it structurally.
|
|
21
60
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
61
|
+
export interface CursorParams {
|
|
62
|
+
limit?: number;
|
|
63
|
+
before?: string;
|
|
64
|
+
after?: string | number;
|
|
65
|
+
}
|
|
24
66
|
/**
|
|
25
67
|
* Walk every page and yield every item.
|
|
26
68
|
*
|
|
27
69
|
* Belt and braces, because an infinite loop in someone's job runner is a bad
|
|
28
|
-
* way to learn about a contract change:
|
|
70
|
+
* way to learn about a contract change, and a silent truncation is a worse one:
|
|
29
71
|
*
|
|
30
|
-
* - stops
|
|
72
|
+
* - stops when the server says `hasMore: false`, on the endpoints that say it;
|
|
31
73
|
* - stops if a page comes back empty, because there is nothing to advance to;
|
|
74
|
+
* - stops when there is no next cursor, which is how the endpoints without
|
|
75
|
+
* `hasMore` signal the end;
|
|
32
76
|
* - stops if the cursor does not MOVE, which is what a non-null terminal
|
|
33
77
|
* cursor looks like from here.
|
|
34
78
|
*/
|
|
35
|
-
export declare function autoPaginate<TItem extends
|
|
36
|
-
id: string;
|
|
37
|
-
}, TParams extends PaginationParams>(fetchPage: PageFetcher<TItem, TParams>, params: TParams): AsyncGenerator<TItem, void, undefined>;
|
|
79
|
+
export declare function autoPaginate<TItem, TParams extends CursorParams>(fetchPage: PageFetcher<TItem, TParams>, params: TParams, cursorParam?: CursorParam): AsyncGenerator<TItem, void, undefined>;
|
|
38
80
|
/**
|
|
39
81
|
* Collect an auto-paginated stream into an array.
|
|
40
82
|
*
|
package/dist/pagination.js
CHANGED
|
@@ -1,54 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keyset pagination.
|
|
3
3
|
*
|
|
4
|
-
* The API pages by cursor rather than by offset
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The API pages by cursor rather than by offset. Most lists take `limit` +
|
|
5
|
+
* `before` and answer `{ data, hasMore, nextCursor }` or `{ data, nextCursor }`,
|
|
6
|
+
* where `before` is the id of the last row you received — which is what
|
|
7
|
+
* `nextCursor` hands you.
|
|
7
8
|
*
|
|
8
|
-
* THE
|
|
9
|
+
* THE TWO TRAPS THIS MODULE EXISTS TO CLOSE
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* returns a non-null cursor on its final page — the documentation calls it out
|
|
13
|
-
* for `/v1/inbound/messages`. A cursor loop over such an endpoint asks for the
|
|
14
|
-
* page after the last one, gets an empty page with the same cursor back, and
|
|
15
|
-
* spins forever. Nothing about it looks wrong in a log; it just never finishes.
|
|
11
|
+
* They are mirror images, which is why neither `hasMore` nor `nextCursor` alone
|
|
12
|
+
* is a correct stopping rule across the whole API:
|
|
16
13
|
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* 1. `while (nextCursor)` spins for ever on `/v1/inbound/messages`. Its cursor
|
|
15
|
+
* is derived from the last row of the page and is set whenever the page has
|
|
16
|
+
* any rows at all, including on the final one. A loop over it asks for the
|
|
17
|
+
* page after the last, gets an empty page carrying the same cursor, and
|
|
18
|
+
* never finishes. Nothing about it looks wrong in a log.
|
|
19
|
+
*
|
|
20
|
+
* 2. `if (!page.hasMore) return` stops after ONE page on `/v1/domains`,
|
|
21
|
+
* `/v1/webhooks`, `/v1/api-keys`, `/v1/inbound/addresses`,
|
|
22
|
+
* `/v1/billing/invoices` and `/v1/billing/history`. Those endpoints send no
|
|
23
|
+
* `hasMore` at all, so the read is `undefined`, which is falsy, which looks
|
|
24
|
+
* exactly like "that was everything". This is the failure that shipped: an
|
|
25
|
+
* account with sixty invoices got fifty of them and no indication that ten
|
|
26
|
+
* were missing.
|
|
27
|
+
*
|
|
28
|
+
* So the walker consults BOTH, and treats each as authoritative only where the
|
|
29
|
+
* server actually sends it: stop when `hasMore` is explicitly `false`, and stop
|
|
30
|
+
* when the cursor is absent or fails to advance. Every endpoint is covered by at
|
|
31
|
+
* least one of those, and no endpoint is stopped early by either.
|
|
21
32
|
*/
|
|
22
33
|
/**
|
|
23
34
|
* Walk every page and yield every item.
|
|
24
35
|
*
|
|
25
36
|
* Belt and braces, because an infinite loop in someone's job runner is a bad
|
|
26
|
-
* way to learn about a contract change:
|
|
37
|
+
* way to learn about a contract change, and a silent truncation is a worse one:
|
|
27
38
|
*
|
|
28
|
-
* - stops
|
|
39
|
+
* - stops when the server says `hasMore: false`, on the endpoints that say it;
|
|
29
40
|
* - stops if a page comes back empty, because there is nothing to advance to;
|
|
41
|
+
* - stops when there is no next cursor, which is how the endpoints without
|
|
42
|
+
* `hasMore` signal the end;
|
|
30
43
|
* - stops if the cursor does not MOVE, which is what a non-null terminal
|
|
31
44
|
* cursor looks like from here.
|
|
32
45
|
*/
|
|
33
|
-
export async function* autoPaginate(fetchPage, params) {
|
|
34
|
-
let
|
|
46
|
+
export async function* autoPaginate(fetchPage, params, cursorParam = 'before') {
|
|
47
|
+
let cursor = params[cursorParam];
|
|
35
48
|
let seen;
|
|
36
49
|
for (;;) {
|
|
37
|
-
const page = await fetchPage({ ...params,
|
|
50
|
+
const page = await fetchPage({ ...params, [cursorParam]: cursor });
|
|
38
51
|
for (const item of page.data)
|
|
39
52
|
yield item;
|
|
40
|
-
|
|
53
|
+
// `=== false`, never `!page.hasMore`: the endpoints that omit the field
|
|
54
|
+
// must fall through to the cursor check rather than be stopped here.
|
|
55
|
+
if (page.hasMore === false)
|
|
41
56
|
return;
|
|
42
57
|
if (page.data.length === 0)
|
|
43
58
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
/*
|
|
60
|
+
* Prefer the server's cursor. Fall back to the last row's id — which is
|
|
61
|
+
* what the cursor is anyway on every `before`-paged list — but ONLY when
|
|
62
|
+
* the server has affirmatively said there is another page.
|
|
63
|
+
*
|
|
64
|
+
* That condition is load-bearing, not caution. On the endpoints with no
|
|
65
|
+
* `hasMore`, a null `nextCursor` IS the end-of-list signal; an unconditional
|
|
66
|
+
* fallback would quietly replace it with the last row's id, and the walker
|
|
67
|
+
* would re-request the final page before the "cursor did not move" guard
|
|
68
|
+
* caught it. The fallback exists only for the opposite case — a page that
|
|
69
|
+
* claims `hasMore: true` and sends no cursor — so it is scoped to exactly
|
|
70
|
+
* that.
|
|
71
|
+
*
|
|
72
|
+
* Rows on `/v1/billing/history` have a `seq` and no `id`, so there is
|
|
73
|
+
* nothing to fall back to there in any case. Correct rather than lucky:
|
|
74
|
+
* that endpoint's `nextCursor` is null exactly on the last page.
|
|
75
|
+
*/
|
|
76
|
+
const last = page.data[page.data.length - 1];
|
|
77
|
+
const next = page.nextCursor ?? (page.hasMore === true ? last?.id : undefined);
|
|
78
|
+
// Either way the NEXT request must ask for something different from the
|
|
79
|
+
// last one.
|
|
80
|
+
if (next == null || next === seen)
|
|
49
81
|
return;
|
|
50
82
|
seen = next;
|
|
51
|
-
|
|
83
|
+
cursor = next;
|
|
52
84
|
}
|
|
53
85
|
}
|
|
54
86
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { AccountId, ApiKeyId, DomainId, InvoiceId, MessageId, PaymentId, SubscriptionId, SuppressionId, WebhookId } from './ids.js';
|
|
14
14
|
/** `message_status` in the database. The lifecycle a message can be in. */
|
|
15
|
-
export declare const MESSAGE_STATUSES: readonly ['queued', 'sending', 'delivered', 'bounced', 'complained', 'failed', 'rejected'];
|
|
15
|
+
export declare const MESSAGE_STATUSES: readonly ['queued', 'sending', 'delivered', 'bounced', 'complained', 'failed', 'scheduled', 'canceled', 'rejected'];
|
|
16
16
|
export type MessageStatus = (typeof MESSAGE_STATUSES)[number];
|
|
17
17
|
/**
|
|
18
|
-
* The
|
|
18
|
+
* The webhook event types (`event_type` in the database).
|
|
19
19
|
*
|
|
20
20
|
* Note that these are NOT the same set as `MessageStatus`: `accepted`,
|
|
21
21
|
* `attempted`, `deferred` and `suppressed` are things that happen to a message
|
|
22
|
-
* without changing the status it rests in.
|
|
22
|
+
* without changing the status it rests in. The `schedule_*` pair and
|
|
23
|
+
* `scheduled` narrate a scheduled message's life before it enters delivery.
|
|
23
24
|
*/
|
|
24
|
-
export declare const EVENT_TYPES: readonly ['accepted', 'queued', 'attempted', 'delivered', 'deferred', 'bounced', 'complained', 'failed', 'rejected', 'suppressed'];
|
|
25
|
+
export declare const EVENT_TYPES: readonly ['accepted', 'queued', 'attempted', 'delivered', 'deferred', 'bounced', 'complained', 'failed', 'rejected', 'suppressed', 'scheduled', 'schedule_canceled', 'schedule_failed'];
|
|
25
26
|
export type EventType = (typeof EVENT_TYPES)[number];
|
|
26
27
|
/**
|
|
27
28
|
* `suppression_reason` in the database.
|
|
@@ -54,25 +55,65 @@ export type KeyEnvironment = 'live' | 'test';
|
|
|
54
55
|
export declare const SCOPES: readonly ['account:read', 'domains:read', 'domains:write', 'emails:send', 'messages:read', 'suppressions:read', 'suppressions:write', 'webhooks:read', 'webhooks:write'];
|
|
55
56
|
export type Scope = (typeof SCOPES)[number];
|
|
56
57
|
/**
|
|
57
|
-
* A keyset page.
|
|
58
|
+
* A keyset page carrying a cursor and nothing else.
|
|
58
59
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
60
|
+
* This is the shape of every list that was RETROFITTED with pagination:
|
|
61
|
+
* `/v1/domains`, `/v1/webhooks`, `/v1/api-keys`, `/v1/inbound/addresses`,
|
|
62
|
+
* `/v1/billing/invoices` and `/v1/billing/history`. They answer
|
|
63
|
+
* `{ nextCursor, data }` and send no `hasMore`.
|
|
64
|
+
*
|
|
65
|
+
* On these, `nextCursor` IS the loop condition and it is trustworthy: the
|
|
66
|
+
* handler asks the database for one row more than you did and sets the cursor
|
|
67
|
+
* only when that extra row came back, so it is null exactly when the page you
|
|
68
|
+
* are holding is the last one.
|
|
69
|
+
*
|
|
70
|
+
* Kept as a type of its own rather than folded into `Page` with an optional
|
|
71
|
+
* `hasMore`, because the two are mirror images and confusing them is the whole
|
|
72
|
+
* bug class. On a `Page` the server's `hasMore` is authoritative and the cursor
|
|
73
|
+
* is not; here there is no `hasMore` to consult at all — and code that reads
|
|
74
|
+
* `page.hasMore` off one of these gets `undefined`, treats it as "no more", and
|
|
75
|
+
* stops after the first page. That is the silent truncation this split exists
|
|
76
|
+
* to make unwritable.
|
|
64
77
|
*/
|
|
65
|
-
export interface
|
|
78
|
+
export interface CursorPage<T> {
|
|
66
79
|
data: T[];
|
|
67
|
-
hasMore: boolean;
|
|
68
80
|
nextCursor: string | null;
|
|
69
81
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A keyset page that also carries the server's own answer to "is there more?".
|
|
84
|
+
*
|
|
85
|
+
* Sent by `/v1/messages`, `/v1/suppressions` and `/v1/inbound/messages` — the
|
|
86
|
+
* lists that were paginated from the start.
|
|
87
|
+
*
|
|
88
|
+
* Here `hasMore` is the ONLY correct loop condition. `nextCursor` is populated
|
|
89
|
+
* from the last row of a page, and `/v1/inbound/messages` returns a non-null
|
|
90
|
+
* cursor on its final page — a `while (nextCursor)` loop over that endpoint
|
|
91
|
+
* never terminates. `autoPaginate` on each resource encodes the right condition
|
|
92
|
+
* so neither half of this can be got wrong by hand.
|
|
93
|
+
*/
|
|
94
|
+
export interface Page<T> extends CursorPage<T> {
|
|
95
|
+
hasMore: boolean;
|
|
73
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated `List` meant "a list the API returns WHOLE, with no cursor".
|
|
99
|
+
* There is no such list any more — every one of them now pages, and a caller
|
|
100
|
+
* that treats the first response as complete silently loses everything past the
|
|
101
|
+
* first page. It is an alias of {@link CursorPage} so existing imports keep
|
|
102
|
+
* compiling; read `nextCursor`, or use the resource's `autoPaginate`.
|
|
103
|
+
*/
|
|
104
|
+
export type List<T> = CursorPage<T>;
|
|
105
|
+
/**
|
|
106
|
+
* The cursor convention shared by every list except `/v1/billing/history`,
|
|
107
|
+
* which reads FORWARD from a sequence number — see
|
|
108
|
+
* {@link ListBillingHistoryParams}.
|
|
109
|
+
*/
|
|
74
110
|
export interface PaginationParams {
|
|
75
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Page size. The bound and the default are per-endpoint: 1–100 default 25 for
|
|
113
|
+
* messages and inbound messages, 1–200 default 50 for suppressions, 1–100
|
|
114
|
+
* default 50 for webhooks, API keys, inbound addresses and invoices, and
|
|
115
|
+
* 1–100 default 100 for domains.
|
|
116
|
+
*/
|
|
76
117
|
limit?: number;
|
|
77
118
|
/** The `nextCursor` from the previous page. */
|
|
78
119
|
before?: string;
|
|
@@ -154,6 +195,16 @@ export interface DnsRecord {
|
|
|
154
195
|
/** Present when publishing the record carries a risk (SPF, DMARC). */
|
|
155
196
|
warning?: string;
|
|
156
197
|
}
|
|
198
|
+
export interface ListDomainsParams extends PaginationParams {
|
|
199
|
+
/**
|
|
200
|
+
* 1–100, default 100.
|
|
201
|
+
*
|
|
202
|
+
* The default is the maximum here, so the first page is every domain on all
|
|
203
|
+
* but the largest accounts — but it is a page, not the whole list, and past
|
|
204
|
+
* a hundred domains `nextCursor` is how you reach the rest.
|
|
205
|
+
*/
|
|
206
|
+
limit?: number;
|
|
207
|
+
}
|
|
157
208
|
export interface CreateDomainParams {
|
|
158
209
|
name: string;
|
|
159
210
|
}
|
|
@@ -237,7 +288,29 @@ export interface CloudflarePublishResult {
|
|
|
237
288
|
}
|
|
238
289
|
export interface SendEmailParams {
|
|
239
290
|
from: string;
|
|
240
|
-
|
|
291
|
+
/**
|
|
292
|
+
* One address, or up to 50 across `to`, `cc` and `bcc` combined.
|
|
293
|
+
*
|
|
294
|
+
* `to: 'x'` and `to: ['x']` are the same request. Every recipient becomes
|
|
295
|
+
* its own message with its own delivery record and its own bounce
|
|
296
|
+
* attribution — which is also why every recipient counts as one send
|
|
297
|
+
* against your allowance. See `cc`.
|
|
298
|
+
*/
|
|
299
|
+
to: string | string[];
|
|
300
|
+
/**
|
|
301
|
+
* Named on the other copies, and charged like any other recipient.
|
|
302
|
+
*
|
|
303
|
+
* A Cc is not a free rider on somebody else's message: it is delivered
|
|
304
|
+
* separately, so a message to one `to` and two `cc` costs three.
|
|
305
|
+
*/
|
|
306
|
+
cc?: string[];
|
|
307
|
+
/**
|
|
308
|
+
* Delivered, and named nowhere.
|
|
309
|
+
*
|
|
310
|
+
* A blind copy is its own message that lists no other recipient — there is
|
|
311
|
+
* no Bcc header on anything we send. Charged like the others.
|
|
312
|
+
*/
|
|
313
|
+
bcc?: string[];
|
|
241
314
|
subject?: string;
|
|
242
315
|
text?: string;
|
|
243
316
|
html?: string;
|
|
@@ -253,6 +326,45 @@ export interface SendEmailParams {
|
|
|
253
326
|
* retry; without it a send is never retried automatically.
|
|
254
327
|
*/
|
|
255
328
|
idempotencyKey?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Up to 10 files, at most 10 MiB combined (decoded). `content` is the raw
|
|
331
|
+
* bytes — pass a `Uint8Array`/`Buffer` and the SDK base64-encodes it, or a
|
|
332
|
+
* string that is ALREADY base64 (it is sent as-is, never double-encoded).
|
|
333
|
+
* Executable types (`.exe`, `.js`, `.bat`, …) are refused by the API with
|
|
334
|
+
* `attachment_type_blocked` — the big mailbox providers bounce them anyway.
|
|
335
|
+
* For an inline image referenced from the html as `<img src="cid:logo">`,
|
|
336
|
+
* set `disposition: 'inline'` and `cid: 'logo'`.
|
|
337
|
+
*/
|
|
338
|
+
attachments?: SendAttachment[];
|
|
339
|
+
/**
|
|
340
|
+
* Send later — ISO-8601 with an offset (what `Date.toISOString()` makes).
|
|
341
|
+
* At most 7 days ahead; within 60 seconds is treated as an immediate send.
|
|
342
|
+
* Quota is decided on the SEND day, and the message is cancellable with
|
|
343
|
+
* `emails.cancelSchedule(id)` until it is released.
|
|
344
|
+
*/
|
|
345
|
+
scheduledAt?: string;
|
|
346
|
+
}
|
|
347
|
+
export interface SendAttachment {
|
|
348
|
+
/** Shown to the recipient. No path separators; 255 chars max. */
|
|
349
|
+
filename: string;
|
|
350
|
+
/** `type/subtype` only — no parameters. */
|
|
351
|
+
contentType: string;
|
|
352
|
+
/** Raw bytes, or a string already base64-encoded. */
|
|
353
|
+
content: Uint8Array | string;
|
|
354
|
+
disposition?: 'attachment' | 'inline';
|
|
355
|
+
/** Content-ID for inline parts, referenced as `cid:` from the html. */
|
|
356
|
+
cid?: string;
|
|
357
|
+
}
|
|
358
|
+
/** What happened to one recipient of a multi-recipient send. */
|
|
359
|
+
export interface SentCopy {
|
|
360
|
+
/** Absent when the recipient was skipped. */
|
|
361
|
+
id?: MessageId;
|
|
362
|
+
to: string;
|
|
363
|
+
/** Which field the address was taken from, after de-duplication. */
|
|
364
|
+
kind: 'to' | 'cc' | 'bcc';
|
|
365
|
+
status: 'queued' | 'scheduled' | 'duplicate' | 'suppressed';
|
|
366
|
+
/** Why it was skipped: `hard_bounce`, `complaint`, `spam_trap`, `manual`. */
|
|
367
|
+
reason?: string;
|
|
256
368
|
}
|
|
257
369
|
export interface SendEmailResult {
|
|
258
370
|
id: MessageId;
|
|
@@ -261,7 +373,7 @@ export interface SendEmailResult {
|
|
|
261
373
|
* (HTTP 200). Both are success, and both carry the id of the one real
|
|
262
374
|
* message.
|
|
263
375
|
*/
|
|
264
|
-
status: 'queued' | 'duplicate';
|
|
376
|
+
status: 'queued' | 'scheduled' | 'duplicate';
|
|
265
377
|
/**
|
|
266
378
|
* True when this request matched an earlier one by `idempotencyKey` and no
|
|
267
379
|
* new message was created. Surfaced rather than hidden, because "we did
|
|
@@ -269,6 +381,30 @@ export interface SendEmailResult {
|
|
|
269
381
|
* answer from "accepted".
|
|
270
382
|
*/
|
|
271
383
|
duplicate: boolean;
|
|
384
|
+
/** Echoed back for a scheduled send. */
|
|
385
|
+
scheduledAt?: string;
|
|
386
|
+
/**
|
|
387
|
+
* Ties the copies of a multi-recipient send together. Absent for a single
|
|
388
|
+
* recipient, which has nothing to be grouped with.
|
|
389
|
+
*/
|
|
390
|
+
groupId?: string;
|
|
391
|
+
/**
|
|
392
|
+
* Every requested recipient, exactly once — present only when there was more
|
|
393
|
+
* than one. `id` is the FIRST To copy and is kept so code reading
|
|
394
|
+
* `{ id, status }` still works, but this array is what is authoritative.
|
|
395
|
+
*/
|
|
396
|
+
emails?: SentCopy[];
|
|
397
|
+
/**
|
|
398
|
+
* Recipients that were skipped because they are suppressed.
|
|
399
|
+
*
|
|
400
|
+
* Repeated here as well as in `emails` deliberately: a recipient we did not
|
|
401
|
+
* send to is the one outcome that must be impossible to miss. A suppressed
|
|
402
|
+
* address is never sent to and never charged.
|
|
403
|
+
*/
|
|
404
|
+
suppressed?: {
|
|
405
|
+
to: string;
|
|
406
|
+
reason: string;
|
|
407
|
+
}[];
|
|
272
408
|
}
|
|
273
409
|
export interface ListMessagesParams extends PaginationParams {
|
|
274
410
|
/** Max 100, default 25. */
|
|
@@ -310,9 +446,26 @@ export interface MessageContent {
|
|
|
310
446
|
raw: string | null;
|
|
311
447
|
replyTo: string | null;
|
|
312
448
|
headers: Record<string, string>;
|
|
449
|
+
/**
|
|
450
|
+
* Metadata only — download the bytes with `emails.downloadAttachment`.
|
|
451
|
+
* Empty array for a message sent without attachments; the whole `content`
|
|
452
|
+
* object is null once the body ages out of retention, attachments with it.
|
|
453
|
+
*/
|
|
454
|
+
attachments: AttachmentMeta[];
|
|
455
|
+
}
|
|
456
|
+
export interface AttachmentMeta {
|
|
457
|
+
/** Bare uuid, used in the download URL. */
|
|
458
|
+
id: string;
|
|
459
|
+
filename: string;
|
|
460
|
+
contentType: string;
|
|
461
|
+
disposition: 'attachment' | 'inline' | (string & {});
|
|
462
|
+
cid: string | null;
|
|
463
|
+
sizeBytes: number;
|
|
313
464
|
}
|
|
314
465
|
export interface Message {
|
|
315
466
|
id: MessageId;
|
|
467
|
+
/** Present on anything that was ever scheduled; `status` says whether it still is. */
|
|
468
|
+
scheduledAt?: string;
|
|
316
469
|
/** The hash chain over this message's own events was re-verified on read. */
|
|
317
470
|
recordIntact: boolean;
|
|
318
471
|
/** Present only when `recordIntact` is false. */
|
|
@@ -417,6 +570,10 @@ export interface CreatedSuppression {
|
|
|
417
570
|
address: string;
|
|
418
571
|
reason: 'manual';
|
|
419
572
|
}
|
|
573
|
+
export interface ListWebhooksParams extends PaginationParams {
|
|
574
|
+
/** 1–100, default 50. */
|
|
575
|
+
limit?: number;
|
|
576
|
+
}
|
|
420
577
|
export interface CreateWebhookParams {
|
|
421
578
|
/** http or https only. */
|
|
422
579
|
url: string;
|
|
@@ -453,6 +610,10 @@ export interface WebhookEvent {
|
|
|
453
610
|
detail?: Record<string, unknown>;
|
|
454
611
|
};
|
|
455
612
|
}
|
|
613
|
+
export interface ListApiKeysParams extends PaginationParams {
|
|
614
|
+
/** 1–100, default 50. Revoked keys are listed too and count towards it. */
|
|
615
|
+
limit?: number;
|
|
616
|
+
}
|
|
456
617
|
export interface ApiKey {
|
|
457
618
|
id: ApiKeyId;
|
|
458
619
|
name: string;
|
|
@@ -549,14 +710,57 @@ export interface BillingEvent {
|
|
|
549
710
|
occurredAt: string;
|
|
550
711
|
hash: string;
|
|
551
712
|
}
|
|
552
|
-
|
|
553
|
-
|
|
713
|
+
/**
|
|
714
|
+
* `/v1/billing/history` pages by SEQUENCE NUMBER, and forwards.
|
|
715
|
+
*
|
|
716
|
+
* It is the one list on this API that does not use `limit` + `before`, and the
|
|
717
|
+
* difference is not cosmetic. The billing record is a hash chain, a chain reads
|
|
718
|
+
* forward from its start, and the order it renders in has to be the order it
|
|
719
|
+
* verifies in — so this endpoint is ascending, oldest first, and resumes from
|
|
720
|
+
* `after` rather than `before`. Everything else on the API is newest-first and
|
|
721
|
+
* resumes from a row id.
|
|
722
|
+
*
|
|
723
|
+
* Two things follow that a caller cannot guess:
|
|
724
|
+
*
|
|
725
|
+
* - `after` is a `BillingEvent.seq` — a number — not a `bil_`-style id. The
|
|
726
|
+
* server rejects anything that is not a non-negative integer with `400
|
|
727
|
+
* invalid_request`.
|
|
728
|
+
* - The page you get back is the events AFTER that sequence number, so the
|
|
729
|
+
* natural resume value is the `seq` of the last event you have processed.
|
|
730
|
+
* `nextCursor` already is exactly that, as a string.
|
|
731
|
+
*/
|
|
732
|
+
export interface ListBillingHistoryParams {
|
|
733
|
+
/** 1–200, default 100. Wider than the rest of the API, because it is a ledger. */
|
|
734
|
+
limit?: number;
|
|
735
|
+
/**
|
|
736
|
+
* A sequence number, NOT an id. Returns the events whose `seq` is strictly
|
|
737
|
+
* greater. `nextCursor` from the previous page is the value to pass.
|
|
738
|
+
*/
|
|
739
|
+
after?: string | number;
|
|
740
|
+
}
|
|
741
|
+
export interface BillingHistory extends CursorPage<BillingEvent> {
|
|
742
|
+
/**
|
|
743
|
+
* The chain replayed in the database, so the record can be checked.
|
|
744
|
+
*
|
|
745
|
+
* Verified over the WHOLE account history, not over the page — so it means
|
|
746
|
+
* the same thing on page four as on page one, and paging does not weaken it.
|
|
747
|
+
*/
|
|
554
748
|
chain: {
|
|
555
749
|
valid: boolean;
|
|
556
750
|
brokenAt: number | null;
|
|
557
751
|
reason: string | null;
|
|
558
752
|
};
|
|
753
|
+
/** Oldest first — the opposite of every other list. */
|
|
559
754
|
data: BillingEvent[];
|
|
755
|
+
/**
|
|
756
|
+
* The `seq` of the last event on this page, as a string, or `null` when this
|
|
757
|
+
* is the last page. Pass it as `after`.
|
|
758
|
+
*/
|
|
759
|
+
nextCursor: string | null;
|
|
760
|
+
}
|
|
761
|
+
export interface ListInvoicesParams extends PaginationParams {
|
|
762
|
+
/** 1–100, default 50. */
|
|
763
|
+
limit?: number;
|
|
560
764
|
}
|
|
561
765
|
export interface Invoice {
|
|
562
766
|
id: InvoiceId;
|
package/dist/types.js
CHANGED
|
@@ -21,14 +21,17 @@ export const MESSAGE_STATUSES = [
|
|
|
21
21
|
'bounced',
|
|
22
22
|
'complained',
|
|
23
23
|
'failed',
|
|
24
|
+
'scheduled',
|
|
25
|
+
'canceled',
|
|
24
26
|
'rejected',
|
|
25
27
|
];
|
|
26
28
|
/**
|
|
27
|
-
* The
|
|
29
|
+
* The webhook event types (`event_type` in the database).
|
|
28
30
|
*
|
|
29
31
|
* Note that these are NOT the same set as `MessageStatus`: `accepted`,
|
|
30
32
|
* `attempted`, `deferred` and `suppressed` are things that happen to a message
|
|
31
|
-
* without changing the status it rests in.
|
|
33
|
+
* without changing the status it rests in. The `schedule_*` pair and
|
|
34
|
+
* `scheduled` narrate a scheduled message's life before it enters delivery.
|
|
32
35
|
*/
|
|
33
36
|
export const EVENT_TYPES = [
|
|
34
37
|
'accepted',
|
|
@@ -41,6 +44,9 @@ export const EVENT_TYPES = [
|
|
|
41
44
|
'failed',
|
|
42
45
|
'rejected',
|
|
43
46
|
'suppressed',
|
|
47
|
+
'scheduled',
|
|
48
|
+
'schedule_canceled',
|
|
49
|
+
'schedule_failed',
|
|
44
50
|
];
|
|
45
51
|
/**
|
|
46
52
|
* `suppression_reason` in the database.
|