@posthaste/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/README.md +454 -0
- package/dist/client.d.ts +234 -0
- package/dist/client.js +459 -0
- package/dist/errors.d.ts +100 -0
- package/dist/errors.js +154 -0
- package/dist/http.d.ts +150 -0
- package/dist/http.js +258 -0
- package/dist/ids.d.ts +25 -0
- package/dist/ids.js +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/pagination.d.ts +45 -0
- package/dist/pagination.js +69 -0
- package/dist/types.d.ts +593 -0
- package/dist/types.js +76 -0
- package/dist/webhooks.d.ts +70 -0
- package/dist/webhooks.js +130 -0
- package/package.json +44 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client.
|
|
3
|
+
*
|
|
4
|
+
* One class, one resource object per area of the API, and every method a thin
|
|
5
|
+
* declaration of a URL, a method, a body and — the only interesting bit —
|
|
6
|
+
* whether repeating the request is safe.
|
|
7
|
+
*
|
|
8
|
+
* Scope: the API-KEY surface only. Endpoints that require a browser session
|
|
9
|
+
* (sign-in, checkout, profile, minting keys) are deliberately absent, because
|
|
10
|
+
* they refuse a Bearer token and an SDK method that can only ever 403 is worse
|
|
11
|
+
* than no method. `/admin/v1/*` is absent for the same reason. `/v1/inbound/*`
|
|
12
|
+
* is absent because a customer cannot use it — see the README.
|
|
13
|
+
*/
|
|
14
|
+
import { HttpClient } from './http.js';
|
|
15
|
+
import { autoPaginate as paginate, collect } from './pagination.js';
|
|
16
|
+
export class Posthaste {
|
|
17
|
+
http;
|
|
18
|
+
account;
|
|
19
|
+
domains;
|
|
20
|
+
emails;
|
|
21
|
+
messages;
|
|
22
|
+
suppressions;
|
|
23
|
+
webhooks;
|
|
24
|
+
apiKeys;
|
|
25
|
+
billing;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.http = new HttpClient(options);
|
|
28
|
+
this.account = new AccountResource(this.http);
|
|
29
|
+
this.domains = new DomainsResource(this.http);
|
|
30
|
+
this.emails = new EmailsResource(this.http);
|
|
31
|
+
this.messages = new MessagesResource(this.http);
|
|
32
|
+
this.suppressions = new SuppressionsResource(this.http);
|
|
33
|
+
this.webhooks = new WebhooksResource(this.http);
|
|
34
|
+
this.apiKeys = new ApiKeysResource(this.http);
|
|
35
|
+
this.billing = new BillingResource(this.http);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Account
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
export class AccountResource {
|
|
42
|
+
http;
|
|
43
|
+
constructor(http) {
|
|
44
|
+
this.http = http;
|
|
45
|
+
}
|
|
46
|
+
/** `GET /v1/me` — who this key belongs to, its scopes, plan and sending caps. */
|
|
47
|
+
me(options) {
|
|
48
|
+
return this.http.request({ method: 'GET', path: '/v1/me', idempotent: true, options });
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `GET /v1/account/verify` — replay the account's ENTIRE event chain.
|
|
52
|
+
*
|
|
53
|
+
* The strong claim, not the per-message one: nothing in the whole delivery
|
|
54
|
+
* history has been altered or removed. Expensive by design; not a health
|
|
55
|
+
* check to run on every request.
|
|
56
|
+
*/
|
|
57
|
+
verify(options) {
|
|
58
|
+
return this.http.request({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
path: '/v1/account/verify',
|
|
61
|
+
idempotent: true,
|
|
62
|
+
options,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** `GET /v1/usage` — the current UTC month, with the daily series behind it. */
|
|
66
|
+
usage(options) {
|
|
67
|
+
return this.http.request({ method: 'GET', path: '/v1/usage', idempotent: true, options });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Domains
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
export class DomainsResource {
|
|
74
|
+
http;
|
|
75
|
+
constructor(http) {
|
|
76
|
+
this.http = http;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `POST /v1/domains` — 201 with the DNS records to publish.
|
|
80
|
+
*
|
|
81
|
+
* Safe to repeat: a second create for the same name is refused with `409
|
|
82
|
+
* conflict` rather than producing a second domain, so a retry after a lost
|
|
83
|
+
* response cannot leave duplicates behind.
|
|
84
|
+
*/
|
|
85
|
+
create(params, options) {
|
|
86
|
+
return this.http.request({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
path: '/v1/domains',
|
|
89
|
+
body: params,
|
|
90
|
+
idempotent: true,
|
|
91
|
+
options,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** `GET /v1/domains` — every domain, newest first. Not paginated. */
|
|
95
|
+
list(options) {
|
|
96
|
+
return this.http.request({ method: 'GET', path: '/v1/domains', idempotent: true, options });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `POST /v1/domains/:id/verify` — look for the records and record the result.
|
|
100
|
+
*
|
|
101
|
+
* Branch on `verified`, not on `status`: `checks` reports SPF and DMARC too,
|
|
102
|
+
* and neither of them failing stops the domain being usable.
|
|
103
|
+
*/
|
|
104
|
+
verify(id, options) {
|
|
105
|
+
return this.http.request({
|
|
106
|
+
method: 'POST',
|
|
107
|
+
path: `/v1/domains/${encodeURIComponent(id)}/verify`,
|
|
108
|
+
idempotent: true,
|
|
109
|
+
options,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `DELETE /v1/domains/:id` — 204.
|
|
114
|
+
*
|
|
115
|
+
* Refused with `409 domain_in_use` while any message references it. That is
|
|
116
|
+
* deliberate: the delivery record is the product, and deleting the domain
|
|
117
|
+
* would take its history with it.
|
|
118
|
+
*/
|
|
119
|
+
delete(id, options) {
|
|
120
|
+
return this.http.requestVoid({
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
path: `/v1/domains/${encodeURIComponent(id)}`,
|
|
123
|
+
idempotent: true,
|
|
124
|
+
options,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** `GET /v1/domains/:id/setup` — who runs this domain's DNS, and whether one-click is available. */
|
|
128
|
+
setup(id, options) {
|
|
129
|
+
return this.http.request({
|
|
130
|
+
method: 'GET',
|
|
131
|
+
path: `/v1/domains/${encodeURIComponent(id)}/setup`,
|
|
132
|
+
idempotent: true,
|
|
133
|
+
options,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* `POST /v1/domains/:id/cloudflare` — publish the DKIM record on the
|
|
138
|
+
* customer's behalf.
|
|
139
|
+
*
|
|
140
|
+
* Only DKIM is written. SPF and DMARC come back under `notPublished`,
|
|
141
|
+
* untouched, because overwriting either is worse than asking somebody to
|
|
142
|
+
* copy a string by hand.
|
|
143
|
+
*
|
|
144
|
+
* Safe to repeat: the record write is an upsert and the token store is an
|
|
145
|
+
* `on conflict do update`.
|
|
146
|
+
*/
|
|
147
|
+
connectCloudflare(id, params = {}, options) {
|
|
148
|
+
return this.http.request({
|
|
149
|
+
method: 'POST',
|
|
150
|
+
path: `/v1/domains/${encodeURIComponent(id)}/cloudflare`,
|
|
151
|
+
body: params,
|
|
152
|
+
idempotent: true,
|
|
153
|
+
options,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `DELETE /v1/account/cloudflare` — forget the stored Cloudflare token.
|
|
158
|
+
*
|
|
159
|
+
* Account-scoped rather than domain-scoped (one token serves every domain),
|
|
160
|
+
* but it lives here because it is part of the DNS story and needs
|
|
161
|
+
* `domains:write`. Always 204, whether a token was stored or not.
|
|
162
|
+
*/
|
|
163
|
+
disconnectCloudflare(options) {
|
|
164
|
+
return this.http.requestVoid({
|
|
165
|
+
method: 'DELETE',
|
|
166
|
+
path: '/v1/account/cloudflare',
|
|
167
|
+
idempotent: true,
|
|
168
|
+
options,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Sending
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
export class EmailsResource {
|
|
176
|
+
http;
|
|
177
|
+
constructor(http) {
|
|
178
|
+
this.http = http;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `POST /v1/emails`.
|
|
182
|
+
*
|
|
183
|
+
* TWO success statuses, and they mean different things:
|
|
184
|
+
*
|
|
185
|
+
* 202 `{ status: 'queued' }` — accepted, a new message exists.
|
|
186
|
+
* 200 `{ status: 'duplicate' }` — an idempotency replay. No new message was
|
|
187
|
+
* created; the id is the original one.
|
|
188
|
+
*
|
|
189
|
+
* Both are returned as a `SendEmailResult` with a `duplicate` boolean, rather
|
|
190
|
+
* than collapsed into "it worked". A caller that bills, logs or counts per
|
|
191
|
+
* send needs to know which of the two happened, and finding out from an HTTP
|
|
192
|
+
* status they never see is not a reasonable ask.
|
|
193
|
+
*
|
|
194
|
+
* RETRIES. A send is only repeated automatically when `idempotencyKey` is
|
|
195
|
+
* set, because without one a retry after a lost response sends the email
|
|
196
|
+
* twice. Setting it is the single most useful thing you can do here.
|
|
197
|
+
*
|
|
198
|
+
* IDEMPOTENCY IS A BODY FIELD. It goes in the JSON as `idempotencyKey`. The
|
|
199
|
+
* `Idempotency-Key` HTTP header is in the API's CORS allowlist but no handler
|
|
200
|
+
* reads it, so a client that sends the header and not the field gets no
|
|
201
|
+
* idempotency at all and no warning that it has none. This SDK never sends
|
|
202
|
+
* the header.
|
|
203
|
+
*/
|
|
204
|
+
async send(params, options) {
|
|
205
|
+
const { status, text } = await this.http.send({
|
|
206
|
+
method: 'POST',
|
|
207
|
+
path: '/v1/emails',
|
|
208
|
+
body: params,
|
|
209
|
+
// The whole rule, in one expression.
|
|
210
|
+
idempotent: Boolean(params.idempotencyKey),
|
|
211
|
+
options,
|
|
212
|
+
});
|
|
213
|
+
const parsed = JSON.parse(text);
|
|
214
|
+
return {
|
|
215
|
+
id: parsed.id,
|
|
216
|
+
status: parsed.status,
|
|
217
|
+
/*
|
|
218
|
+
* Read from the BODY, not from `status === 200`.
|
|
219
|
+
*
|
|
220
|
+
* The body is the authoritative answer and survives a proxy that
|
|
221
|
+
* normalises a 202 to a 200; the status is a corroborating signal, not
|
|
222
|
+
* the source of truth.
|
|
223
|
+
*/
|
|
224
|
+
duplicate: parsed.status === 'duplicate',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Messages
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
export class MessagesResource {
|
|
232
|
+
http;
|
|
233
|
+
constructor(http) {
|
|
234
|
+
this.http = http;
|
|
235
|
+
}
|
|
236
|
+
/** `GET /v1/messages` — one keyset page, newest first. */
|
|
237
|
+
list(params = {}, options) {
|
|
238
|
+
return this.http.request({
|
|
239
|
+
method: 'GET',
|
|
240
|
+
path: '/v1/messages',
|
|
241
|
+
query: params,
|
|
242
|
+
idempotent: true,
|
|
243
|
+
options,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Every message matching the filter, across every page.
|
|
248
|
+
*
|
|
249
|
+
* Loops on `hasMore`. Never write the `while (nextCursor)` version by hand —
|
|
250
|
+
* see `pagination.ts` for why it does not terminate.
|
|
251
|
+
*/
|
|
252
|
+
autoPaginate(params = {}, options) {
|
|
253
|
+
return paginate((p) => this.list(p, options), params);
|
|
254
|
+
}
|
|
255
|
+
/** Drain `autoPaginate` into an array, up to `maxItems`. */
|
|
256
|
+
listAll(params = {}, maxItems = 1000, options) {
|
|
257
|
+
return collect(this.autoPaginate(params, options), maxItems);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* `GET /v1/messages/:id` — the waybill: content, every event, and the hash
|
|
261
|
+
* linkage, with the chain re-verified on read (`recordIntact`).
|
|
262
|
+
*/
|
|
263
|
+
get(id, options) {
|
|
264
|
+
return this.http.request({
|
|
265
|
+
method: 'GET',
|
|
266
|
+
path: `/v1/messages/${encodeURIComponent(id)}`,
|
|
267
|
+
idempotent: true,
|
|
268
|
+
options,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `GET /v1/stats/messages` — daily volume with the previous window for
|
|
273
|
+
* comparison.
|
|
274
|
+
*
|
|
275
|
+
* Rates here are PERCENTAGES over settled mail (`total` minus `pending`).
|
|
276
|
+
* `GET /v1/usage` reports its rates as fractions over everything sent — the
|
|
277
|
+
* two endpoints genuinely differ, and dividing one by 100 to compare them is
|
|
278
|
+
* not enough.
|
|
279
|
+
*/
|
|
280
|
+
stats(params = {}, options) {
|
|
281
|
+
return this.http.request({
|
|
282
|
+
method: 'GET',
|
|
283
|
+
path: '/v1/stats/messages',
|
|
284
|
+
query: params,
|
|
285
|
+
idempotent: true,
|
|
286
|
+
options,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Suppressions
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
export class SuppressionsResource {
|
|
294
|
+
http;
|
|
295
|
+
constructor(http) {
|
|
296
|
+
this.http = http;
|
|
297
|
+
}
|
|
298
|
+
/** `GET /v1/suppressions` — one keyset page. `limit` caps at 200. */
|
|
299
|
+
list(params = {}, options) {
|
|
300
|
+
return this.http.request({
|
|
301
|
+
method: 'GET',
|
|
302
|
+
path: '/v1/suppressions',
|
|
303
|
+
query: params,
|
|
304
|
+
idempotent: true,
|
|
305
|
+
options,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** Every suppression matching the filter. Loops on `hasMore`. */
|
|
309
|
+
autoPaginate(params = {}, options) {
|
|
310
|
+
return paginate((p) => this.list(p, options), params);
|
|
311
|
+
}
|
|
312
|
+
/** Drain `autoPaginate` into an array, up to `maxItems`. */
|
|
313
|
+
listAll(params = {}, maxItems = 1000, options) {
|
|
314
|
+
return collect(this.autoPaginate(params, options), maxItems);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* `POST /v1/suppressions` — 201.
|
|
318
|
+
*
|
|
319
|
+
* `reason` here is free text and is stored as the entry's DETAIL. The entry's
|
|
320
|
+
* own reason is always `manual`; only the platform creates `hard_bounce`,
|
|
321
|
+
* `complaint`, `unsubscribe` and `spam_trap` entries.
|
|
322
|
+
*
|
|
323
|
+
* Safe to repeat: the insert is `on conflict do nothing`, and re-adding an
|
|
324
|
+
* address answers 201 again.
|
|
325
|
+
*/
|
|
326
|
+
create(params, options) {
|
|
327
|
+
return this.http.request({
|
|
328
|
+
method: 'POST',
|
|
329
|
+
path: '/v1/suppressions',
|
|
330
|
+
body: params,
|
|
331
|
+
idempotent: true,
|
|
332
|
+
options,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* `DELETE /v1/suppressions/:address` — 204.
|
|
337
|
+
*
|
|
338
|
+
* Addressed by ADDRESS, not by `sup_` id, and this SDK URL-encodes it for
|
|
339
|
+
* you. Two entries are refused: a `complaint` (422 `suppression_protected` —
|
|
340
|
+
* sending again is what gets an IP blocklisted) and a platform-wide entry
|
|
341
|
+
* (422 `suppression_platform`).
|
|
342
|
+
*/
|
|
343
|
+
delete(address, options) {
|
|
344
|
+
return this.http.requestVoid({
|
|
345
|
+
method: 'DELETE',
|
|
346
|
+
path: `/v1/suppressions/${encodeURIComponent(address)}`,
|
|
347
|
+
idempotent: true,
|
|
348
|
+
options,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Webhooks
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
export class WebhooksResource {
|
|
356
|
+
http;
|
|
357
|
+
constructor(http) {
|
|
358
|
+
this.http = http;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* `POST /v1/webhooks` — 201, carrying `signingSecret`.
|
|
362
|
+
*
|
|
363
|
+
* The secret is shown exactly ONCE and is not retrievable afterwards, because
|
|
364
|
+
* a signing secret that can be re-read is one that anybody with a stolen API
|
|
365
|
+
* key can read too. Store it before you do anything else with the response.
|
|
366
|
+
*
|
|
367
|
+
* NOT auto-retried: repeating this creates a second webhook, and the
|
|
368
|
+
* duplicate would then receive every event twice.
|
|
369
|
+
*/
|
|
370
|
+
create(params, options) {
|
|
371
|
+
return this.http.request({
|
|
372
|
+
method: 'POST',
|
|
373
|
+
path: '/v1/webhooks',
|
|
374
|
+
body: params,
|
|
375
|
+
idempotent: false,
|
|
376
|
+
options,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** `GET /v1/webhooks` — newest first. Not paginated. Never includes secrets. */
|
|
380
|
+
list(options) {
|
|
381
|
+
return this.http.request({ method: 'GET', path: '/v1/webhooks', idempotent: true, options });
|
|
382
|
+
}
|
|
383
|
+
/** `DELETE /v1/webhooks/:id` — 204, or 404 for an unknown id. */
|
|
384
|
+
delete(id, options) {
|
|
385
|
+
return this.http.requestVoid({
|
|
386
|
+
method: 'DELETE',
|
|
387
|
+
path: `/v1/webhooks/${encodeURIComponent(id)}`,
|
|
388
|
+
idempotent: true,
|
|
389
|
+
options,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// API keys
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
export class ApiKeysResource {
|
|
397
|
+
http;
|
|
398
|
+
constructor(http) {
|
|
399
|
+
this.http = http;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* `GET /v1/api-keys` — the 100 most recent keys, revoked ones included.
|
|
403
|
+
*
|
|
404
|
+
* Read only, and that is the whole resource. Creating and revoking keys
|
|
405
|
+
* requires a signed-in owner or admin and refuses a Bearer key outright: a
|
|
406
|
+
* server-side credential that could mint more credentials would make every
|
|
407
|
+
* narrow key one request away from a full one.
|
|
408
|
+
*/
|
|
409
|
+
list(options) {
|
|
410
|
+
return this.http.request({ method: 'GET', path: '/v1/api-keys', idempotent: true, options });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Billing
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
export class BillingResource {
|
|
417
|
+
http;
|
|
418
|
+
constructor(http) {
|
|
419
|
+
this.http = http;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* `GET /v1/billing` — current plan, subscription, profile, recent payments
|
|
423
|
+
* and the full price list.
|
|
424
|
+
*
|
|
425
|
+
* All four billing reads accept `billing:read` OR `account:read`; an API key
|
|
426
|
+
* cannot hold `billing:read`, so in practice `account:read` is the one that
|
|
427
|
+
* gets you in. Money is always in minor units.
|
|
428
|
+
*/
|
|
429
|
+
get(options) {
|
|
430
|
+
return this.http.request({ method: 'GET', path: '/v1/billing', idempotent: true, options });
|
|
431
|
+
}
|
|
432
|
+
/** `GET /v1/billing/history` — the hash-chained commercial record, oldest first, plus its verification. */
|
|
433
|
+
history(options) {
|
|
434
|
+
return this.http.request({
|
|
435
|
+
method: 'GET',
|
|
436
|
+
path: '/v1/billing/history',
|
|
437
|
+
idempotent: true,
|
|
438
|
+
options,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/** `GET /v1/billing/invoices` — the 100 most recent, newest first. Not paginated. */
|
|
442
|
+
invoices(options) {
|
|
443
|
+
return this.http.request({
|
|
444
|
+
method: 'GET',
|
|
445
|
+
path: '/v1/billing/invoices',
|
|
446
|
+
idempotent: true,
|
|
447
|
+
options,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/** `GET /v1/billing/invoices/:id` — the same document plus the supplier block. */
|
|
451
|
+
invoice(id, options) {
|
|
452
|
+
return this.http.request({
|
|
453
|
+
method: 'GET',
|
|
454
|
+
path: `/v1/billing/invoices/${encodeURIComponent(id)}`,
|
|
455
|
+
idempotent: true,
|
|
456
|
+
options,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors.
|
|
3
|
+
*
|
|
4
|
+
* Every failure this SDK can produce arrives as a `PosthasteError`, including
|
|
5
|
+
* the ones that never touched the server (a socket that would not open, a
|
|
6
|
+
* request that timed out). One `catch` clause, one shape.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The `error.type` values the API emits.
|
|
10
|
+
*
|
|
11
|
+
* Listed as a union so a `switch` is exhaustive where it can be, and widened
|
|
12
|
+
* with `(string & {})` so a type this SDK version has not heard of still
|
|
13
|
+
* type-checks — a new refusal reason must not be a compile error for anybody
|
|
14
|
+
* who has not upgraded.
|
|
15
|
+
*/
|
|
16
|
+
export type KnownErrorType = 'unauthorized' | 'unauthenticated' | 'forbidden' | 'csrf_failed' | 'email_unverified' | 'invalid_request' | 'not_found' | 'conflict'
|
|
17
|
+
/** Inbound addresses. Not reachable through this SDK — see the README. */
|
|
18
|
+
| 'address_taken' | 'invalid_address' | 'domain_not_found' | 'domain_not_verified' | 'suppressed' | 'rate_limited' | 'daily_limit_reached' | 'monthly_limit_reached' | 'domain_limit_reached' | 'domain_in_use' | 'token_required' | 'cloudflare_token_invalid' | 'cloudflare_zone_not_found' | 'cloudflare_write_failed' | 'suppression_protected' | 'suppression_platform' | 'not_configured' | 'provider_error' | 'already_subscribed' | 'internal' | 'unknown_error' | 'connection_error' | 'timeout';
|
|
19
|
+
export type PosthasteErrorType = KnownErrorType | (string & {});
|
|
20
|
+
/** One entry of `error.fields`, present on some — not all — 400s. */
|
|
21
|
+
export interface FieldError {
|
|
22
|
+
/** Dotted path into the request body, e.g. `headers.x-thing`. */
|
|
23
|
+
path: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PosthasteErrorInit {
|
|
27
|
+
status: number;
|
|
28
|
+
type: PosthasteErrorType;
|
|
29
|
+
message: string;
|
|
30
|
+
fields?: FieldError[];
|
|
31
|
+
retryAfterSeconds?: number;
|
|
32
|
+
/** The parsed response body, exactly as it arrived. */
|
|
33
|
+
body?: unknown;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export declare class PosthasteError extends Error {
|
|
37
|
+
readonly name = "PosthasteError";
|
|
38
|
+
/**
|
|
39
|
+
* The HTTP status. `0` when the request never got a response at all — a DNS
|
|
40
|
+
* failure, a refused connection, an abort. Checking `status >= 500` is
|
|
41
|
+
* therefore not a substitute for checking `type`.
|
|
42
|
+
*/
|
|
43
|
+
readonly status: number;
|
|
44
|
+
readonly type: PosthasteErrorType;
|
|
45
|
+
/**
|
|
46
|
+
* Field-level detail, when the server sent any.
|
|
47
|
+
*
|
|
48
|
+
* `error.fields` appears on the 400s produced by the shared body validator
|
|
49
|
+
* and is ABSENT on the several 400s that are hand-written with a message
|
|
50
|
+
* only. Always treat it as optional; never index into it unchecked.
|
|
51
|
+
*/
|
|
52
|
+
readonly fields?: FieldError[];
|
|
53
|
+
/**
|
|
54
|
+
* How long to wait, in seconds, when the server said. Taken from
|
|
55
|
+
* `error.retryAfterSeconds` (rate limiting) or the `Retry-After` header
|
|
56
|
+
* (quota exhaustion), in that order.
|
|
57
|
+
*/
|
|
58
|
+
readonly retryAfterSeconds?: number;
|
|
59
|
+
/** The parsed body. `undefined` when there was nothing parseable. */
|
|
60
|
+
readonly body?: unknown;
|
|
61
|
+
constructor(init: PosthasteErrorInit);
|
|
62
|
+
/**
|
|
63
|
+
* True for the two refusals that mean "your allowance is spent", as opposed
|
|
64
|
+
* to "you are going too fast".
|
|
65
|
+
*
|
|
66
|
+
* Both arrive as 429 with a `Retry-After`, and the status alone cannot tell
|
|
67
|
+
* them apart — which is exactly the trap that makes a naive retry loop hammer
|
|
68
|
+
* a wall for the rest of the month. This SDK never retries these in-process.
|
|
69
|
+
*/
|
|
70
|
+
get isQuotaExhausted(): boolean;
|
|
71
|
+
/** True for the per-key request-rate limiter, which is short and transient. */
|
|
72
|
+
get isRateLimited(): boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function isPosthasteError(value: unknown): value is PosthasteError;
|
|
75
|
+
/**
|
|
76
|
+
* Turn a response body into an error.
|
|
77
|
+
*
|
|
78
|
+
* The API's own refusals use `{ error: { type, message, ... } }`. Two kinds of
|
|
79
|
+
* response do NOT, and both are parsed defensively here rather than assumed
|
|
80
|
+
* away:
|
|
81
|
+
*
|
|
82
|
+
* An UNHANDLED 500. There is no `setErrorHandler` on the API, so a thrown
|
|
83
|
+
* exception is serialised by Fastify's default handler as
|
|
84
|
+
* `{ statusCode, error: "Internal Server Error", message }` — `error` is a
|
|
85
|
+
* STRING. Reading `body.error.type` off that yields `undefined` and reading
|
|
86
|
+
* `body.error.message` throws, which would turn the one response where a
|
|
87
|
+
* caller most needs a clear message into a crash inside the SDK.
|
|
88
|
+
*
|
|
89
|
+
* Anything that never reached the application at all — a proxy's HTML error
|
|
90
|
+
* page, an empty body, a truncated response.
|
|
91
|
+
*
|
|
92
|
+
* Neither invents a `type`: both become `unknown_error`, so nothing downstream
|
|
93
|
+
* can mistake an unclassified failure for a documented one.
|
|
94
|
+
*/
|
|
95
|
+
export declare function errorFromResponse(status: number, rawBody: string, retryAfterHeader: string | null): PosthasteError;
|
|
96
|
+
/**
|
|
97
|
+
* `Retry-After` is either a number of seconds or an HTTP date. The API sends
|
|
98
|
+
* seconds; a proxy in front of it may not.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseRetryAfter(header: string | null): number | undefined;
|