@sendora/sdk 1.1.0 → 2.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 +152 -70
- package/dist/account.d.ts +30 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +25 -0
- package/dist/account.js.map +1 -0
- package/dist/client.d.ts +13 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -30
- package/dist/client.js.map +1 -1
- package/dist/domains.d.ts +1 -1
- package/dist/domains.d.ts.map +1 -1
- package/dist/domains.js +1 -1
- package/dist/domains.js.map +1 -1
- package/dist/error.d.ts +6 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +22 -2
- package/dist/error.js.map +1 -1
- package/dist/inbound-domains.d.ts +36 -0
- package/dist/inbound-domains.d.ts.map +1 -0
- package/dist/inbound-domains.js +72 -0
- package/dist/inbound-domains.js.map +1 -0
- package/dist/inbound.d.ts +61 -0
- package/dist/inbound.d.ts.map +1 -0
- package/dist/inbound.js +106 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +20 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +34 -0
- package/dist/options.js.map +1 -0
- package/dist/servers.d.ts +64 -0
- package/dist/servers.d.ts.map +1 -0
- package/dist/servers.js +133 -0
- package/dist/servers.js.map +1 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +15 -5
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +182 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook-verify.d.ts +7 -5
- package/dist/webhook-verify.d.ts.map +1 -1
- package/dist/webhook-verify.js +38 -17
- package/dist/webhook-verify.js.map +1 -1
- package/dist/webhooks.d.ts +13 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +26 -0
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -9
- package/skills/sendora/SKILL.md +19 -9
- package/src/account.ts +40 -0
- package/src/client.ts +20 -43
- package/src/domains.ts +1 -1
- package/src/error.ts +36 -3
- package/src/inbound-domains.ts +89 -0
- package/src/inbound.ts +135 -0
- package/src/index.ts +8 -1
- package/src/options.ts +51 -0
- package/src/servers.ts +166 -0
- package/src/transport.ts +26 -5
- package/src/types.ts +194 -5
- package/src/version.ts +1 -1
- package/src/webhook-verify.ts +39 -18
- package/src/webhooks.ts +29 -0
package/dist/webhook-verify.js
CHANGED
|
@@ -15,11 +15,13 @@ const encoder = new TextEncoder();
|
|
|
15
15
|
const decoder = new TextDecoder();
|
|
16
16
|
/**
|
|
17
17
|
* Checks a webhook request came from Sendora and answers its event, typed
|
|
18
|
-
* by `event`. The header is `Sendora-Signature: t=<unix seconds>,v1=<hex
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* by `event`. The header is `Sendora-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]`,
|
|
19
|
+
* one hex per live secret of the webhook, each an HMAC-SHA256 with that
|
|
20
|
+
* secret over `<t>.<raw body>`; the one your secret produces is enough, so
|
|
21
|
+
* a secret being rolled in verifies as well as the old one. The timestamp
|
|
22
|
+
* is inside the signed text, so a captured request cannot be replayed
|
|
23
|
+
* after the tolerance. Throws `SendoraError` with `invalid_signature` or
|
|
24
|
+
* `stale_signature`.
|
|
23
25
|
*
|
|
24
26
|
* @example
|
|
25
27
|
* const event = await verifyWebhook({
|
|
@@ -48,32 +50,51 @@ export async function verifyWebhook(input) {
|
|
|
48
50
|
const body = typeof input.body === 'string' ? encoder.encode(input.body) : input.body;
|
|
49
51
|
const signed = concat(encoder.encode(`${String(signature.timestamp)}.`), body);
|
|
50
52
|
const key = await crypto.subtle.importKey('raw', encoder.encode(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// Every signature is checked, so the time taken says nothing about which one matched.
|
|
54
|
+
let matched = false;
|
|
55
|
+
for (const mac of signature.macs) {
|
|
56
|
+
matched = (await crypto.subtle.verify('HMAC', key, mac, signed)) || matched;
|
|
57
|
+
}
|
|
58
|
+
if (!matched) {
|
|
59
|
+
throw invalidSignature('No signature in the header matches the body under this secret.');
|
|
53
60
|
}
|
|
54
61
|
return parseEvent(decoder.decode(body));
|
|
55
62
|
}
|
|
63
|
+
/** The timestamp and every `v1` value, one per live secret of the webhook. */
|
|
56
64
|
function parseSignature(header) {
|
|
57
65
|
if (typeof header !== 'string' || header === '') {
|
|
58
66
|
throw invalidSignature('The request carries no Sendora-Signature header.');
|
|
59
67
|
}
|
|
60
|
-
|
|
68
|
+
let timestamp;
|
|
69
|
+
const macs = [];
|
|
61
70
|
for (const part of header.split(',')) {
|
|
62
71
|
const separator = part.indexOf('=');
|
|
63
|
-
if (separator
|
|
64
|
-
|
|
72
|
+
if (separator <= 0) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const name = part.slice(0, separator).trim();
|
|
76
|
+
const value = part.slice(separator + 1).trim();
|
|
77
|
+
if (name === 't') {
|
|
78
|
+
timestamp = Number(value);
|
|
79
|
+
}
|
|
80
|
+
else if (name === 'v1') {
|
|
81
|
+
if (!MAC_HEX.test(value)) {
|
|
82
|
+
throw invalidSignature('The Sendora-Signature header is malformed.');
|
|
83
|
+
}
|
|
84
|
+
macs.push(bytesOf(value));
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
|
-
|
|
68
|
-
const hex = parts.get('v1');
|
|
69
|
-
if (!Number.isInteger(timestamp) || hex === undefined || !MAC_HEX.test(hex)) {
|
|
87
|
+
if (timestamp === undefined || !Number.isInteger(timestamp) || macs.length === 0) {
|
|
70
88
|
throw invalidSignature('The Sendora-Signature header is malformed.');
|
|
71
89
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
return { timestamp, macs };
|
|
91
|
+
}
|
|
92
|
+
function bytesOf(hex) {
|
|
93
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
94
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
95
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
75
96
|
}
|
|
76
|
-
return
|
|
97
|
+
return bytes;
|
|
77
98
|
}
|
|
78
99
|
function parseEvent(text) {
|
|
79
100
|
let value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-verify.js","sourceRoot":"","sources":["../src/webhook-verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAgB1C,MAAM,yBAAyB,GAAG,GAAG,CAAC;AACtC,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAElC,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAmB;IAChE,WAAW;IACX,SAAS;IACT,UAAU;IACV,gBAAgB;IAChB,cAAc;IACd,aAAa;IACb,aAAa;IACb,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC
|
|
1
|
+
{"version":3,"file":"webhook-verify.js","sourceRoot":"","sources":["../src/webhook-verify.ts"],"names":[],"mappings":"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"}
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Transport } from './transport.ts';
|
|
2
|
-
import type { CreatedWebhook, CreateWebhookRequest, Delivery, DeliveryPage, DeliveryQuery, ReceiveOptions, RequestOptions, Webhook, WebhookEvent, WebhookList } from './types.ts';
|
|
2
|
+
import type { CreatedWebhook, CreatedWebhookSecret, CreateWebhookRequest, Delivery, DeliveryPage, DeliveryQuery, ReceiveOptions, RequestOptions, Webhook, WebhookEvent, WebhookList } from './types.ts';
|
|
3
3
|
/** Where the server's events are posted, every delivery of them, and the receiving side. */
|
|
4
4
|
export declare class WebhooksResource {
|
|
5
5
|
#private;
|
|
@@ -23,6 +23,18 @@ export declare class WebhooksResource {
|
|
|
23
23
|
get(webhookId: string, options?: RequestOptions): Promise<Webhook>;
|
|
24
24
|
/** Removes the webhook; deliveries still pending are dropped with it. */
|
|
25
25
|
delete(webhookId: string, options?: RequestOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Adds a second live secret and answers its value once. Every delivery
|
|
28
|
+
* then carries one signature per live secret, the newest first, so
|
|
29
|
+
* switch your receiver to the new secret and delete the old one; a third
|
|
30
|
+
* is refused with `secret_limit`.
|
|
31
|
+
*
|
|
32
|
+
*
|
|
33
|
+
* const { secret, secretId } = await sendora.webhooks.createSecret(webhookId);
|
|
34
|
+
*/
|
|
35
|
+
createSecret(webhookId: string, options?: RequestOptions): Promise<CreatedWebhookSecret>;
|
|
36
|
+
/** Deletes a secret; deliveries are no longer signed with it. The last live secret is refused (`last_secret`). */
|
|
37
|
+
deleteSecret(webhookId: string, secretId: string, options?: RequestOptions): Promise<void>;
|
|
26
38
|
/**
|
|
27
39
|
* One page of the events handed to the webhook, newest first, with the
|
|
28
40
|
* outcome of the last attempt; `status` narrows to `pending`,
|
package/dist/webhooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,cAAc,EACd,cAAc,EACd,OAAO,EACP,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAGpB,4FAA4F;AAC5F,qBAAa,gBAAgB;;gBAGf,SAAS,EAAE,SAAS;IAIhC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5F,mCAAmC;IACnC,IAAI,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;IASxD,yBAAyB;IACzB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAStE,yEAAyE;IACzE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAStE;;;;;;;OAOG;IACH,UAAU,CACR,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,aAAkB,EACzB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC;IAUxB,uEAAuE;IACvE,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,aAAkB,EACzB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAAC,QAAQ,CAAC;IAS1B;;;;;;;OAOG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS9F;;;;;;;;;;;;;;OAcG;IACG,OAAO,CACX,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC;CAQzB"}
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,cAAc,EACd,cAAc,EACd,OAAO,EACP,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAGpB,4FAA4F;AAC5F,qBAAa,gBAAgB;;gBAGf,SAAS,EAAE,SAAS;IAIhC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5F,mCAAmC;IACnC,IAAI,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;IASxD,yBAAyB;IACzB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAStE,yEAAyE;IACzE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAStE;;;;;;;;OAQG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAS5F,kHAAkH;IAClH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9F;;;;;;;OAOG;IACH,UAAU,CACR,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,aAAkB,EACzB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC;IAUxB,uEAAuE;IACvE,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,aAAkB,EACzB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAAC,QAAQ,CAAC;IAS1B;;;;;;;OAOG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS9F;;;;;;;;;;;;;;OAcG;IACG,OAAO,CACX,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC;CAQzB"}
|
package/dist/webhooks.js
CHANGED
|
@@ -54,6 +54,32 @@ export class WebhooksResource {
|
|
|
54
54
|
signal: options.signal,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Adds a second live secret and answers its value once. Every delivery
|
|
59
|
+
* then carries one signature per live secret, the newest first, so
|
|
60
|
+
* switch your receiver to the new secret and delete the old one; a third
|
|
61
|
+
* is refused with `secret_limit`.
|
|
62
|
+
*
|
|
63
|
+
*
|
|
64
|
+
* const { secret, secretId } = await sendora.webhooks.createSecret(webhookId);
|
|
65
|
+
*/
|
|
66
|
+
createSecret(webhookId, options = {}) {
|
|
67
|
+
return this.#transport.request({
|
|
68
|
+
method: 'POST',
|
|
69
|
+
path: `/v1/webhooks/${encodeURIComponent(webhookId)}/secrets`,
|
|
70
|
+
idempotent: false,
|
|
71
|
+
signal: options.signal,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Deletes a secret; deliveries are no longer signed with it. The last live secret is refused (`last_secret`). */
|
|
75
|
+
deleteSecret(webhookId, secretId, options = {}) {
|
|
76
|
+
return this.#transport.request({
|
|
77
|
+
method: 'DELETE',
|
|
78
|
+
path: `/v1/webhooks/${encodeURIComponent(webhookId)}/secrets/${encodeURIComponent(secretId)}`,
|
|
79
|
+
idempotent: true,
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
57
83
|
/**
|
|
58
84
|
* One page of the events handed to the webhook, newest first, with the
|
|
59
85
|
* outcome of the last attempt; `status` narrows to `pending`,
|
package/dist/webhooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendora/sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "The official TypeScript SDK for Sendora, transactional email delivery
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "The official TypeScript SDK for Sendora, transactional email delivery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -21,11 +21,6 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20.19"
|
|
23
23
|
},
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/hornta/sendora.git",
|
|
27
|
-
"directory": "sdks/typescript"
|
|
28
|
-
},
|
|
29
24
|
"homepage": "https://sendora.se/docs",
|
|
30
25
|
"keywords": [
|
|
31
26
|
"sendora",
|
|
@@ -38,10 +33,10 @@
|
|
|
38
33
|
"typescript": "~6.0.3",
|
|
39
34
|
"vitest": "^4.1.11",
|
|
40
35
|
"zod": "^4.6.2",
|
|
36
|
+
"@sendora/observability": "0.0.0",
|
|
41
37
|
"@sendora/api": "0.0.0",
|
|
42
|
-
"@sendora/db": "0.0.0",
|
|
43
38
|
"@sendora/domain": "0.0.0",
|
|
44
|
-
"@sendora/
|
|
39
|
+
"@sendora/db": "0.0.0"
|
|
45
40
|
},
|
|
46
41
|
"scripts": {
|
|
47
42
|
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|
package/skills/sendora/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sendora
|
|
3
|
-
description: Send transactional email through Sendora with @sendora/sdk, read the message log, manage streams, suppressions,
|
|
3
|
+
description: Send transactional email through Sendora with @sendora/sdk, read the message log, manage streams, suppressions, keys, webhooks, servers and sending domains, and receive signed webhooks. Use when a task mentions Sendora, @sendora/sdk, or sending email from a system hosted in Sweden.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Sendora
|
|
@@ -12,12 +12,20 @@ complete reference; every call, every error code and every event is in it.
|
|
|
12
12
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
- Two kinds of key, two clients. A server key (`sk_…`) drives
|
|
16
|
+
`new Sendora({ token })`: `email`, `messages`, `inbound`, `streams`,
|
|
17
|
+
`suppressions`, `tokens`, `webhooks`, `inboundDomains`, everything inside
|
|
18
|
+
one server. An account key (`ak_…`) drives `new SendoraAccount({ token })`:
|
|
19
|
+
`servers`, `servers.tokens` and `domains`, and it never sends. A key of
|
|
20
|
+
the wrong kind throws `wrong_token_kind`.
|
|
21
|
+
- Every key is a secret. Read it from the environment on the server and
|
|
22
|
+
never send it to a browser. Either client throws when its key is
|
|
23
|
+
missing, so construct it at startup.
|
|
24
|
+
- A server is created with its first key: `account.servers.create({ name })`
|
|
25
|
+
answers `token.token` once, and `new Sendora({ token })` sends with it. A
|
|
26
|
+
server or the account holds at most two live keys, so rotate by creating
|
|
27
|
+
the new key, switching to it, then revoking the old one; the last live
|
|
28
|
+
key can never be revoked.
|
|
21
29
|
- Every method takes what the route takes and answers what the route
|
|
22
30
|
answers. Do not build request bodies by hand or call `fetch` yourself.
|
|
23
31
|
- Do not add idempotency keys or retry loops of your own. The SDK gives
|
|
@@ -40,8 +48,10 @@ complete reference; every call, every error code and every event is in it.
|
|
|
40
48
|
`suppressions.delete({ address })`; a spam complaint cannot be lifted by
|
|
41
49
|
the customer. Lists are per stream: pass `streamId` when the send went on
|
|
42
50
|
a stream other than the default.
|
|
43
|
-
- `from` must be on a verified sending domain
|
|
44
|
-
the two DNS records to add, and
|
|
51
|
+
- `from` must be on a verified sending domain of the account;
|
|
52
|
+
`account.domains.create` answers the two DNS records to add, and
|
|
53
|
+
`account.domains.verify` checks them now. A server key cannot manage
|
|
54
|
+
domains.
|
|
45
55
|
- A message goes on the server's default transactional stream unless it
|
|
46
56
|
names another with `streamId`; `streams.create` adds one. Never make a
|
|
47
57
|
stream per message or per customer; a stream is a kind of mail.
|
package/src/account.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DomainsResource } from './domains.ts';
|
|
2
|
+
import { transportFor, type ClientOptions } from './options.ts';
|
|
3
|
+
import { ServersResource } from './servers.ts';
|
|
4
|
+
|
|
5
|
+
export interface SendoraAccountOptions extends ClientOptions {
|
|
6
|
+
/**
|
|
7
|
+
* An account key (`ak_…`), created by an administrator in the dashboard.
|
|
8
|
+
* It manages the account and never sends. Keep it on the server; never
|
|
9
|
+
* ship it to a browser. `undefined`, as an unset environment variable
|
|
10
|
+
* gives, throws at construction rather than at the first call.
|
|
11
|
+
*/
|
|
12
|
+
token: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The account client: the servers, each server's keys and the sending
|
|
17
|
+
* domains, under an account key. Sending, the log, streams, suppressions
|
|
18
|
+
* and webhooks belong to a server and to `Sendora` with a server key.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { Sendora, SendoraAccount } from '@sendora/sdk';
|
|
22
|
+
*
|
|
23
|
+
* const account = new SendoraAccount({ token: process.env.SENDORA_ACCOUNT_TOKEN });
|
|
24
|
+
* const server = await account.servers.create({ name: 'Fakturering' });
|
|
25
|
+
* const sendora = new Sendora({ token: server.token.token });
|
|
26
|
+
*/
|
|
27
|
+
export class SendoraAccount {
|
|
28
|
+
readonly servers: ServersResource;
|
|
29
|
+
readonly domains: DomainsResource;
|
|
30
|
+
|
|
31
|
+
constructor(options: SendoraAccountOptions) {
|
|
32
|
+
const transport = transportFor(
|
|
33
|
+
options.token,
|
|
34
|
+
options,
|
|
35
|
+
'SendoraAccount needs an account key (ak_…); an administrator creates one in the dashboard.',
|
|
36
|
+
);
|
|
37
|
+
this.servers = new ServersResource(transport);
|
|
38
|
+
this.domains = new DomainsResource(transport);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
import { DomainsResource } from './domains.ts';
|
|
2
1
|
import { BroadcastsResource } from './broadcasts.ts';
|
|
3
2
|
import { EmailResource } from './email.ts';
|
|
3
|
+
import { InboundResource } from './inbound.ts';
|
|
4
|
+
import { InboundDomainsResource } from './inbound-domains.ts';
|
|
4
5
|
import { MessagesResource } from './messages.ts';
|
|
5
|
-
import {
|
|
6
|
+
import { transportFor, type ClientOptions } from './options.ts';
|
|
6
7
|
import { StreamsResource } from './streams.ts';
|
|
7
8
|
import { SuppressionsResource } from './suppressions.ts';
|
|
8
9
|
import { TokensResource } from './tokens.ts';
|
|
9
|
-
import { Transport } from './transport.ts';
|
|
10
|
-
import { SDK_VERSION } from './version.ts';
|
|
11
10
|
import { WebhooksResource } from './webhooks.ts';
|
|
12
11
|
|
|
13
|
-
export
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
-
|
|
16
|
-
export interface SendoraOptions {
|
|
12
|
+
export interface SendoraOptions extends ClientOptions {
|
|
17
13
|
/**
|
|
18
|
-
* A server
|
|
19
|
-
* never ship it to a browser.
|
|
20
|
-
* variable gives, throws at
|
|
14
|
+
* A server key (`sk_…`), answered once when the server is created or a
|
|
15
|
+
* key is added to it. Keep it on the server; never ship it to a browser.
|
|
16
|
+
* `undefined`, as an unset environment variable gives, throws at
|
|
17
|
+
* construction rather than at the first call.
|
|
21
18
|
*/
|
|
22
19
|
token: string | undefined;
|
|
23
|
-
/** The API's origin; https://api.sendora.se unless you test against another. */
|
|
24
|
-
baseUrl?: string | undefined;
|
|
25
|
-
/** The fetch to use; the global one unless you need a proxy or a fake. */
|
|
26
|
-
fetch?: typeof fetch | undefined;
|
|
27
|
-
/** How long one attempt may take; 30 seconds by default. */
|
|
28
|
-
timeoutMs?: number | undefined;
|
|
29
|
-
/** How many times a failed call is repeated when repeating is safe; 2 by default, 0 turns retries off. */
|
|
30
|
-
maxRetries?: number | undefined;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
/**
|
|
34
|
-
* The client. One instance per server
|
|
23
|
+
* The server client. One instance per server key; every resource of the
|
|
24
|
+
* server hangs off it. The account's servers and sending domains are
|
|
25
|
+
* `SendoraAccount` with an account key.
|
|
35
26
|
*
|
|
36
27
|
* @example
|
|
37
28
|
* import { Sendora } from '@sendora/sdk';
|
|
@@ -48,41 +39,27 @@ export class Sendora {
|
|
|
48
39
|
readonly email: EmailResource;
|
|
49
40
|
readonly broadcasts: BroadcastsResource;
|
|
50
41
|
readonly messages: MessagesResource;
|
|
42
|
+
readonly inbound: InboundResource;
|
|
43
|
+
readonly inboundDomains: InboundDomainsResource;
|
|
51
44
|
readonly streams: StreamsResource;
|
|
52
45
|
readonly suppressions: SuppressionsResource;
|
|
53
46
|
readonly tokens: TokensResource;
|
|
54
47
|
readonly webhooks: WebhooksResource;
|
|
55
|
-
readonly domains: DomainsResource;
|
|
56
48
|
|
|
57
49
|
constructor(options: SendoraOptions) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
64
|
-
if (!(timeoutMs > 0)) {
|
|
65
|
-
throw new TypeError('timeoutMs must be a positive number of milliseconds.');
|
|
66
|
-
}
|
|
67
|
-
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
68
|
-
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
69
|
-
throw new TypeError('maxRetries must be a whole number of zero or more.');
|
|
70
|
-
}
|
|
71
|
-
const transport = new Transport({
|
|
72
|
-
baseUrl,
|
|
73
|
-
token: options.token,
|
|
74
|
-
fetch: options.fetch ?? fetch,
|
|
75
|
-
timeoutMs,
|
|
76
|
-
maxRetries,
|
|
77
|
-
userAgent: `sendora-sdk/${SDK_VERSION}`,
|
|
78
|
-
});
|
|
50
|
+
const transport = transportFor(
|
|
51
|
+
options.token,
|
|
52
|
+
options,
|
|
53
|
+
'Sendora needs a server key (sk_…); it is shown once when the server or the key is created.',
|
|
54
|
+
);
|
|
79
55
|
this.email = new EmailResource(transport);
|
|
80
56
|
this.broadcasts = new BroadcastsResource(transport);
|
|
81
57
|
this.messages = new MessagesResource(transport);
|
|
58
|
+
this.inbound = new InboundResource(transport);
|
|
59
|
+
this.inboundDomains = new InboundDomainsResource(transport);
|
|
82
60
|
this.streams = new StreamsResource(transport);
|
|
83
61
|
this.suppressions = new SuppressionsResource(transport);
|
|
84
62
|
this.tokens = new TokensResource(transport);
|
|
85
63
|
this.webhooks = new WebhooksResource(transport);
|
|
86
|
-
this.domains = new DomainsResource(transport);
|
|
87
64
|
}
|
|
88
65
|
}
|
package/src/domains.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
VerifiedDomain,
|
|
8
8
|
} from './types.ts';
|
|
9
9
|
|
|
10
|
-
/** The account's sending domains and the two DNS records each one needs
|
|
10
|
+
/** The account's sending domains and the two DNS records each one needs, managed with the account key; every server sends from them. */
|
|
11
11
|
export class DomainsResource {
|
|
12
12
|
readonly #transport: Transport;
|
|
13
13
|
|
package/src/error.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { LimitScope, SuppressedRecipient, ValidationIssue } from './types.ts';
|
|
2
2
|
|
|
3
|
-
/** Every code the API answers, the three the SDK raises when it never got a proper answer, and the two of the webhook verifier. */
|
|
3
|
+
/** Every code the API answers, under either kind of key, the three the SDK raises when it never got a proper answer, and the two of the webhook verifier. */
|
|
4
4
|
export type SendoraErrorCode =
|
|
5
5
|
| 'invalid_request'
|
|
6
6
|
| 'unauthorized'
|
|
7
|
+
| 'wrong_token_kind'
|
|
7
8
|
| 'payment_required'
|
|
8
9
|
| 'tenant_paused'
|
|
9
10
|
| 'tenant_not_active'
|
|
@@ -11,11 +12,20 @@ export type SendoraErrorCode =
|
|
|
11
12
|
| 'unsubscribe_locked'
|
|
12
13
|
| 'not_found'
|
|
13
14
|
| 'domain_exists'
|
|
15
|
+
| 'domain_reserved'
|
|
16
|
+
| 'inbound_domain_exists'
|
|
14
17
|
| 'webhook_exists'
|
|
15
18
|
| 'stream_exists'
|
|
16
19
|
| 'inbound_stream_exists'
|
|
17
20
|
| 'default_stream'
|
|
18
21
|
| 'last_token'
|
|
22
|
+
| 'token_limit'
|
|
23
|
+
| 'last_secret'
|
|
24
|
+
| 'secret_limit'
|
|
25
|
+
| 'server_exists'
|
|
26
|
+
| 'server_limit'
|
|
27
|
+
| 'server_reserved'
|
|
28
|
+
| 'server_in_flight'
|
|
19
29
|
| 'request_too_large'
|
|
20
30
|
| 'from_domain_not_verified'
|
|
21
31
|
| 'stream_not_found'
|
|
@@ -28,6 +38,8 @@ export type SendoraErrorCode =
|
|
|
28
38
|
| 'substitution_missing'
|
|
29
39
|
| 'unsubscribe_placeholder_missing'
|
|
30
40
|
| 'list_unsubscribe_reserved'
|
|
41
|
+
| 'content_expired'
|
|
42
|
+
| 'content_unreadable'
|
|
31
43
|
| 'recipient_suppressed'
|
|
32
44
|
| 'idempotency_key_mismatch'
|
|
33
45
|
| 'idempotency_key_required'
|
|
@@ -43,6 +55,7 @@ export type SendoraErrorCode =
|
|
|
43
55
|
const apiCodes: ReadonlySet<string> = new Set<SendoraErrorCode>([
|
|
44
56
|
'invalid_request',
|
|
45
57
|
'unauthorized',
|
|
58
|
+
'wrong_token_kind',
|
|
46
59
|
'payment_required',
|
|
47
60
|
'tenant_paused',
|
|
48
61
|
'tenant_not_active',
|
|
@@ -50,11 +63,20 @@ const apiCodes: ReadonlySet<string> = new Set<SendoraErrorCode>([
|
|
|
50
63
|
'unsubscribe_locked',
|
|
51
64
|
'not_found',
|
|
52
65
|
'domain_exists',
|
|
66
|
+
'domain_reserved',
|
|
67
|
+
'inbound_domain_exists',
|
|
53
68
|
'webhook_exists',
|
|
54
69
|
'stream_exists',
|
|
55
70
|
'inbound_stream_exists',
|
|
56
71
|
'default_stream',
|
|
57
72
|
'last_token',
|
|
73
|
+
'token_limit',
|
|
74
|
+
'last_secret',
|
|
75
|
+
'secret_limit',
|
|
76
|
+
'server_exists',
|
|
77
|
+
'server_limit',
|
|
78
|
+
'server_reserved',
|
|
79
|
+
'server_in_flight',
|
|
58
80
|
'request_too_large',
|
|
59
81
|
'from_domain_not_verified',
|
|
60
82
|
'stream_not_found',
|
|
@@ -67,6 +89,8 @@ const apiCodes: ReadonlySet<string> = new Set<SendoraErrorCode>([
|
|
|
67
89
|
'substitution_missing',
|
|
68
90
|
'unsubscribe_placeholder_missing',
|
|
69
91
|
'list_unsubscribe_reserved',
|
|
92
|
+
'content_expired',
|
|
93
|
+
'content_unreadable',
|
|
70
94
|
'recipient_suppressed',
|
|
71
95
|
'idempotency_key_mismatch',
|
|
72
96
|
'idempotency_key_required',
|
|
@@ -85,6 +109,7 @@ export interface SendoraErrorFields {
|
|
|
85
109
|
cap?: number | null;
|
|
86
110
|
used?: number | null;
|
|
87
111
|
resetsAt?: string | null;
|
|
112
|
+
max?: number | null;
|
|
88
113
|
issues?: ValidationIssue[];
|
|
89
114
|
suppressed?: SuppressedRecipient[];
|
|
90
115
|
streamId?: string | null;
|
|
@@ -124,13 +149,15 @@ export class SendoraError extends Error {
|
|
|
124
149
|
readonly used: number | null;
|
|
125
150
|
/** When the month counter resets, on monthly_cap_reached. */
|
|
126
151
|
readonly resetsAt: string | null;
|
|
152
|
+
/** Live keys the server or the account may hold, on token_limit; servers the account may have, on server_limit; live secrets a webhook may hold, on secret_limit. */
|
|
153
|
+
readonly max: number | null;
|
|
127
154
|
/** One entry per invalid field, on invalid_request. */
|
|
128
155
|
readonly issues: ValidationIssue[];
|
|
129
156
|
/** The recipients the server refused, on recipient_suppressed. */
|
|
130
157
|
readonly suppressed: SuppressedRecipient[];
|
|
131
158
|
/** The stream whose suppression list refused the send, on recipient_suppressed. */
|
|
132
159
|
readonly streamId: string | null;
|
|
133
|
-
/** The id of the domain, webhook or stream that already exists, on domain_exists, webhook_exists and
|
|
160
|
+
/** The id of the domain, webhook or stream that already exists, on domain_exists, webhook_exists, stream_exists, inbound_stream_exists and, when the stream already has a domain, inbound_domain_exists. */
|
|
134
161
|
readonly existingId: string | null;
|
|
135
162
|
|
|
136
163
|
constructor(fields: SendoraErrorFields) {
|
|
@@ -143,6 +170,7 @@ export class SendoraError extends Error {
|
|
|
143
170
|
this.cap = fields.cap ?? null;
|
|
144
171
|
this.used = fields.used ?? null;
|
|
145
172
|
this.resetsAt = fields.resetsAt ?? null;
|
|
173
|
+
this.max = fields.max ?? null;
|
|
146
174
|
this.issues = fields.issues ?? [];
|
|
147
175
|
this.suppressed = fields.suppressed ?? [];
|
|
148
176
|
this.streamId = fields.streamId ?? null;
|
|
@@ -172,6 +200,7 @@ export class SendoraError extends Error {
|
|
|
172
200
|
cap: this.cap,
|
|
173
201
|
used: this.used,
|
|
174
202
|
resetsAt: this.resetsAt,
|
|
203
|
+
max: this.max,
|
|
175
204
|
issues: this.issues,
|
|
176
205
|
suppressed: this.suppressed,
|
|
177
206
|
streamId: this.streamId,
|
|
@@ -205,11 +234,15 @@ export function errorFromAnswer(
|
|
|
205
234
|
cap: integerOf(answer.cap),
|
|
206
235
|
used: integerOf(answer.used),
|
|
207
236
|
resetsAt: typeof answer.resetsAt === 'string' ? answer.resetsAt : null,
|
|
237
|
+
max: integerOf(answer.max) ?? integerOf(answer.maxServers),
|
|
208
238
|
issues,
|
|
209
239
|
suppressed: Array.isArray(answer.suppressed) ? answer.suppressed.filter(isSuppressed) : [],
|
|
210
240
|
streamId: code === 'recipient_suppressed' ? stringOf(answer.streamId) : null,
|
|
211
241
|
existingId:
|
|
212
|
-
stringOf(answer.domainId) ??
|
|
242
|
+
stringOf(answer.domainId) ??
|
|
243
|
+
stringOf(answer.webhookId) ??
|
|
244
|
+
stringOf(answer.streamId) ??
|
|
245
|
+
stringOf(answer.inboundDomainId),
|
|
213
246
|
});
|
|
214
247
|
}
|
|
215
248
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Transport } from './transport.ts';
|
|
2
|
+
import type {
|
|
3
|
+
CreateInboundDomainRequest,
|
|
4
|
+
InboundDomain,
|
|
5
|
+
InboundDomainList,
|
|
6
|
+
RequestOptions,
|
|
7
|
+
VerifiedInboundDomain,
|
|
8
|
+
} from './types.ts';
|
|
9
|
+
|
|
10
|
+
/** The server's own domains for receiving: one per inbound stream, catch-all, with the two DNS records each one needs. */
|
|
11
|
+
export class InboundDomainsResource {
|
|
12
|
+
readonly #transport: Transport;
|
|
13
|
+
|
|
14
|
+
constructor(transport: Transport) {
|
|
15
|
+
this.#transport = transport;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Claims a domain for an inbound stream and answers the two records to
|
|
20
|
+
* create: an MX that brings the domain's mail to Sendora and a TXT
|
|
21
|
+
* record that proves the claim. Mail is accepted once both are seen.
|
|
22
|
+
* `domain_reserved` is a name of Sendora's own; `inbound_domain_exists`
|
|
23
|
+
* is a stream that already has a domain (`existingId` names it) or a
|
|
24
|
+
* name another account holds verified.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const domain = await sendora.inboundDomains.create({ streamId, domain: 'post.example.se' });
|
|
28
|
+
* console.log(domain.mx.host, domain.mx.value);
|
|
29
|
+
* console.log(domain.txt.host, domain.txt.value);
|
|
30
|
+
*/
|
|
31
|
+
create(
|
|
32
|
+
request: CreateInboundDomainRequest,
|
|
33
|
+
options: RequestOptions = {},
|
|
34
|
+
): Promise<InboundDomain> {
|
|
35
|
+
return this.#transport.request<InboundDomain>({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
path: '/v1/inbound/domains',
|
|
38
|
+
body: request,
|
|
39
|
+
idempotent: false,
|
|
40
|
+
signal: options.signal,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Every inbound domain of the token's server. */
|
|
45
|
+
list(options: RequestOptions = {}): Promise<InboundDomainList> {
|
|
46
|
+
return this.#transport.request<InboundDomainList>({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
path: '/v1/inbound/domains',
|
|
49
|
+
idempotent: true,
|
|
50
|
+
signal: options.signal,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One domain by id, with the state of its records. */
|
|
55
|
+
get(inboundDomainId: string, options: RequestOptions = {}): Promise<InboundDomain> {
|
|
56
|
+
return this.#transport.request<InboundDomain>({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: `/v1/inbound/domains/${encodeURIComponent(inboundDomainId)}`,
|
|
59
|
+
idempotent: true,
|
|
60
|
+
signal: options.signal,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Looks both records up now and answers the domain as it stands plus
|
|
66
|
+
* what each lookup found: `ok`, `missing`, `mismatch` or `dns_error`.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const { verified, check } = await sendora.inboundDomains.verify(inboundDomainId);
|
|
70
|
+
*/
|
|
71
|
+
verify(inboundDomainId: string, options: RequestOptions = {}): Promise<VerifiedInboundDomain> {
|
|
72
|
+
return this.#transport.request<VerifiedInboundDomain>({
|
|
73
|
+
method: 'POST',
|
|
74
|
+
path: `/v1/inbound/domains/${encodeURIComponent(inboundDomainId)}/verify`,
|
|
75
|
+
idempotent: true,
|
|
76
|
+
signal: options.signal,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Removes the domain; mail to it is refused from then on, and the name is free for another claim. */
|
|
81
|
+
delete(inboundDomainId: string, options: RequestOptions = {}): Promise<void> {
|
|
82
|
+
return this.#transport.request<undefined>({
|
|
83
|
+
method: 'DELETE',
|
|
84
|
+
path: `/v1/inbound/domains/${encodeURIComponent(inboundDomainId)}`,
|
|
85
|
+
idempotent: true,
|
|
86
|
+
signal: options.signal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|