@sendora/sdk 1.2.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 +87 -26
- 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 +9 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -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 +5 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +13 -0
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +6 -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/types.d.ts +40 -3
- 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 -4
- package/skills/sendora/SKILL.md +19 -9
- package/src/account.ts +40 -0
- package/src/client.ts +14 -43
- package/src/domains.ts +1 -1
- package/src/error.ts +23 -1
- package/src/index.ts +6 -1
- package/src/options.ts +51 -0
- package/src/servers.ts +166 -0
- package/src/types.ts +47 -3
- package/src/version.ts +1 -1
- package/src/webhook-verify.ts +39 -18
- package/src/webhooks.ts +29 -0
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendora/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The official TypeScript SDK for Sendora, transactional email delivery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"typescript": "~6.0.3",
|
|
34
34
|
"vitest": "^4.1.11",
|
|
35
35
|
"zod": "^4.6.2",
|
|
36
|
-
"@sendora/
|
|
37
|
-
"@sendora/domain": "0.0.0",
|
|
36
|
+
"@sendora/observability": "0.0.0",
|
|
38
37
|
"@sendora/api": "0.0.0",
|
|
39
|
-
"@sendora/
|
|
38
|
+
"@sendora/domain": "0.0.0",
|
|
39
|
+
"@sendora/db": "0.0.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
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,39 +1,28 @@
|
|
|
1
|
-
import { DomainsResource } from './domains.ts';
|
|
2
1
|
import { BroadcastsResource } from './broadcasts.ts';
|
|
3
2
|
import { EmailResource } from './email.ts';
|
|
4
3
|
import { InboundResource } from './inbound.ts';
|
|
5
4
|
import { InboundDomainsResource } from './inbound-domains.ts';
|
|
6
5
|
import { MessagesResource } from './messages.ts';
|
|
7
|
-
import {
|
|
6
|
+
import { transportFor, type ClientOptions } from './options.ts';
|
|
8
7
|
import { StreamsResource } from './streams.ts';
|
|
9
8
|
import { SuppressionsResource } from './suppressions.ts';
|
|
10
9
|
import { TokensResource } from './tokens.ts';
|
|
11
|
-
import { Transport } from './transport.ts';
|
|
12
|
-
import { SDK_VERSION } from './version.ts';
|
|
13
10
|
import { WebhooksResource } from './webhooks.ts';
|
|
14
11
|
|
|
15
|
-
export
|
|
16
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
|
-
|
|
18
|
-
export interface SendoraOptions {
|
|
12
|
+
export interface SendoraOptions extends ClientOptions {
|
|
19
13
|
/**
|
|
20
|
-
* A server
|
|
21
|
-
* never ship it to a browser.
|
|
22
|
-
* 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.
|
|
23
18
|
*/
|
|
24
19
|
token: string | undefined;
|
|
25
|
-
/** The API's origin; https://api.sendora.se unless you test against another. */
|
|
26
|
-
baseUrl?: string | undefined;
|
|
27
|
-
/** The fetch to use; the global one unless you need a proxy or a fake. */
|
|
28
|
-
fetch?: typeof fetch | undefined;
|
|
29
|
-
/** How long one attempt may take; 30 seconds by default. */
|
|
30
|
-
timeoutMs?: number | undefined;
|
|
31
|
-
/** How many times a failed call is repeated when repeating is safe; 2 by default, 0 turns retries off. */
|
|
32
|
-
maxRetries?: number | undefined;
|
|
33
20
|
}
|
|
34
21
|
|
|
35
22
|
/**
|
|
36
|
-
* 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.
|
|
37
26
|
*
|
|
38
27
|
* @example
|
|
39
28
|
* import { Sendora } from '@sendora/sdk';
|
|
@@ -56,30 +45,13 @@ export class Sendora {
|
|
|
56
45
|
readonly suppressions: SuppressionsResource;
|
|
57
46
|
readonly tokens: TokensResource;
|
|
58
47
|
readonly webhooks: WebhooksResource;
|
|
59
|
-
readonly domains: DomainsResource;
|
|
60
48
|
|
|
61
49
|
constructor(options: SendoraOptions) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
68
|
-
if (!(timeoutMs > 0)) {
|
|
69
|
-
throw new TypeError('timeoutMs must be a positive number of milliseconds.');
|
|
70
|
-
}
|
|
71
|
-
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
72
|
-
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
73
|
-
throw new TypeError('maxRetries must be a whole number of zero or more.');
|
|
74
|
-
}
|
|
75
|
-
const transport = new Transport({
|
|
76
|
-
baseUrl,
|
|
77
|
-
token: options.token,
|
|
78
|
-
fetch: options.fetch ?? fetch,
|
|
79
|
-
timeoutMs,
|
|
80
|
-
maxRetries,
|
|
81
|
-
userAgent: `sendora-sdk/${SDK_VERSION}`,
|
|
82
|
-
});
|
|
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
|
+
);
|
|
83
55
|
this.email = new EmailResource(transport);
|
|
84
56
|
this.broadcasts = new BroadcastsResource(transport);
|
|
85
57
|
this.messages = new MessagesResource(transport);
|
|
@@ -89,6 +61,5 @@ export class Sendora {
|
|
|
89
61
|
this.suppressions = new SuppressionsResource(transport);
|
|
90
62
|
this.tokens = new TokensResource(transport);
|
|
91
63
|
this.webhooks = new WebhooksResource(transport);
|
|
92
|
-
this.domains = new DomainsResource(transport);
|
|
93
64
|
}
|
|
94
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'
|
|
@@ -18,6 +19,13 @@ export type SendoraErrorCode =
|
|
|
18
19
|
| 'inbound_stream_exists'
|
|
19
20
|
| 'default_stream'
|
|
20
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'
|
|
21
29
|
| 'request_too_large'
|
|
22
30
|
| 'from_domain_not_verified'
|
|
23
31
|
| 'stream_not_found'
|
|
@@ -47,6 +55,7 @@ export type SendoraErrorCode =
|
|
|
47
55
|
const apiCodes: ReadonlySet<string> = new Set<SendoraErrorCode>([
|
|
48
56
|
'invalid_request',
|
|
49
57
|
'unauthorized',
|
|
58
|
+
'wrong_token_kind',
|
|
50
59
|
'payment_required',
|
|
51
60
|
'tenant_paused',
|
|
52
61
|
'tenant_not_active',
|
|
@@ -61,6 +70,13 @@ const apiCodes: ReadonlySet<string> = new Set<SendoraErrorCode>([
|
|
|
61
70
|
'inbound_stream_exists',
|
|
62
71
|
'default_stream',
|
|
63
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',
|
|
64
80
|
'request_too_large',
|
|
65
81
|
'from_domain_not_verified',
|
|
66
82
|
'stream_not_found',
|
|
@@ -93,6 +109,7 @@ export interface SendoraErrorFields {
|
|
|
93
109
|
cap?: number | null;
|
|
94
110
|
used?: number | null;
|
|
95
111
|
resetsAt?: string | null;
|
|
112
|
+
max?: number | null;
|
|
96
113
|
issues?: ValidationIssue[];
|
|
97
114
|
suppressed?: SuppressedRecipient[];
|
|
98
115
|
streamId?: string | null;
|
|
@@ -132,6 +149,8 @@ export class SendoraError extends Error {
|
|
|
132
149
|
readonly used: number | null;
|
|
133
150
|
/** When the month counter resets, on monthly_cap_reached. */
|
|
134
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;
|
|
135
154
|
/** One entry per invalid field, on invalid_request. */
|
|
136
155
|
readonly issues: ValidationIssue[];
|
|
137
156
|
/** The recipients the server refused, on recipient_suppressed. */
|
|
@@ -151,6 +170,7 @@ export class SendoraError extends Error {
|
|
|
151
170
|
this.cap = fields.cap ?? null;
|
|
152
171
|
this.used = fields.used ?? null;
|
|
153
172
|
this.resetsAt = fields.resetsAt ?? null;
|
|
173
|
+
this.max = fields.max ?? null;
|
|
154
174
|
this.issues = fields.issues ?? [];
|
|
155
175
|
this.suppressed = fields.suppressed ?? [];
|
|
156
176
|
this.streamId = fields.streamId ?? null;
|
|
@@ -180,6 +200,7 @@ export class SendoraError extends Error {
|
|
|
180
200
|
cap: this.cap,
|
|
181
201
|
used: this.used,
|
|
182
202
|
resetsAt: this.resetsAt,
|
|
203
|
+
max: this.max,
|
|
183
204
|
issues: this.issues,
|
|
184
205
|
suppressed: this.suppressed,
|
|
185
206
|
streamId: this.streamId,
|
|
@@ -213,6 +234,7 @@ export function errorFromAnswer(
|
|
|
213
234
|
cap: integerOf(answer.cap),
|
|
214
235
|
used: integerOf(answer.used),
|
|
215
236
|
resetsAt: typeof answer.resetsAt === 'string' ? answer.resetsAt : null,
|
|
237
|
+
max: integerOf(answer.max) ?? integerOf(answer.maxServers),
|
|
216
238
|
issues,
|
|
217
239
|
suppressed: Array.isArray(answer.suppressed) ? answer.suppressed.filter(isSuppressed) : [],
|
|
218
240
|
streamId: code === 'recipient_suppressed' ? stringOf(answer.streamId) : null,
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { SendoraAccount } from './account.ts';
|
|
2
|
+
export type { SendoraAccountOptions } from './account.ts';
|
|
3
|
+
export { Sendora } from './client.ts';
|
|
2
4
|
export type { SendoraOptions } from './client.ts';
|
|
3
5
|
export type { BroadcastsResource } from './broadcasts.ts';
|
|
4
6
|
export type { DomainsResource } from './domains.ts';
|
|
@@ -8,6 +10,9 @@ export type { SendoraErrorCode } from './error.ts';
|
|
|
8
10
|
export type { InboundResource } from './inbound.ts';
|
|
9
11
|
export type { InboundDomainsResource } from './inbound-domains.ts';
|
|
10
12
|
export type { MessagesResource } from './messages.ts';
|
|
13
|
+
export { DEFAULT_BASE_URL } from './options.ts';
|
|
14
|
+
export type { ClientOptions } from './options.ts';
|
|
15
|
+
export type { ServersResource, ServerTokensResource } from './servers.ts';
|
|
11
16
|
export type { StreamsResource } from './streams.ts';
|
|
12
17
|
export type { SuppressionsResource } from './suppressions.ts';
|
|
13
18
|
export type { TokensResource } from './tokens.ts';
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DEFAULT_MAX_RETRIES } from './retry.ts';
|
|
2
|
+
import { Transport } from './transport.ts';
|
|
3
|
+
import { SDK_VERSION } from './version.ts';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_BASE_URL = 'https://api.sendora.se';
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
|
|
8
|
+
/** What both clients take besides the key. */
|
|
9
|
+
export interface ClientOptions {
|
|
10
|
+
/** The API's origin; https://api.sendora.se unless you test against another. */
|
|
11
|
+
baseUrl?: string | undefined;
|
|
12
|
+
/** The fetch to use; the global one unless you need a proxy or a fake. */
|
|
13
|
+
fetch?: typeof fetch | undefined;
|
|
14
|
+
/** How long one attempt may take; 30 seconds by default. */
|
|
15
|
+
timeoutMs?: number | undefined;
|
|
16
|
+
/** How many times a failed call is repeated when repeating is safe; 2 by default, 0 turns retries off. */
|
|
17
|
+
maxRetries?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The transport a client's resources share, built once at construction so
|
|
22
|
+
* a missing key or a nonsense option fails at startup rather than at the
|
|
23
|
+
* first call.
|
|
24
|
+
*/
|
|
25
|
+
export function transportFor(
|
|
26
|
+
token: string | undefined,
|
|
27
|
+
options: ClientOptions,
|
|
28
|
+
missingKey: string,
|
|
29
|
+
): Transport {
|
|
30
|
+
if (token === undefined || token.trim() === '') {
|
|
31
|
+
throw new TypeError(missingKey);
|
|
32
|
+
}
|
|
33
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
34
|
+
new URL(baseUrl);
|
|
35
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
if (!(timeoutMs > 0)) {
|
|
37
|
+
throw new TypeError('timeoutMs must be a positive number of milliseconds.');
|
|
38
|
+
}
|
|
39
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
40
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
41
|
+
throw new TypeError('maxRetries must be a whole number of zero or more.');
|
|
42
|
+
}
|
|
43
|
+
return new Transport({
|
|
44
|
+
baseUrl,
|
|
45
|
+
token,
|
|
46
|
+
fetch: options.fetch ?? fetch,
|
|
47
|
+
timeoutMs,
|
|
48
|
+
maxRetries,
|
|
49
|
+
userAgent: `sendora-sdk/${SDK_VERSION}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/src/servers.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { Transport } from './transport.ts';
|
|
2
|
+
import type {
|
|
3
|
+
CreatedServer,
|
|
4
|
+
CreatedToken,
|
|
5
|
+
CreateServerRequest,
|
|
6
|
+
CreateTokenRequest,
|
|
7
|
+
RequestOptions,
|
|
8
|
+
Server,
|
|
9
|
+
ServerList,
|
|
10
|
+
Token,
|
|
11
|
+
TokenList,
|
|
12
|
+
UpdateServerRequest,
|
|
13
|
+
} from './types.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The keys of one server, managed with the account key. A server holds at
|
|
17
|
+
* most two live keys, so rotation is create, switch, revoke.
|
|
18
|
+
*/
|
|
19
|
+
export class ServerTokensResource {
|
|
20
|
+
readonly #transport: Transport;
|
|
21
|
+
|
|
22
|
+
constructor(transport: Transport) {
|
|
23
|
+
this.#transport = transport;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates another live key for the server and answers its value once; it
|
|
28
|
+
* is never shown again, so store it at once. A third live key is refused
|
|
29
|
+
* with `token_limit`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const { token } = await account.servers.tokens.create(serverId, { name: 'Fakturasystemet' });
|
|
33
|
+
*/
|
|
34
|
+
create(
|
|
35
|
+
serverId: string,
|
|
36
|
+
request: CreateTokenRequest,
|
|
37
|
+
options: RequestOptions = {},
|
|
38
|
+
): Promise<CreatedToken> {
|
|
39
|
+
return this.#transport.request<CreatedToken>({
|
|
40
|
+
method: 'POST',
|
|
41
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}/tokens`,
|
|
42
|
+
body: request,
|
|
43
|
+
idempotent: false,
|
|
44
|
+
signal: options.signal,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Every key of the server, revoked ones included. */
|
|
49
|
+
list(serverId: string, options: RequestOptions = {}): Promise<TokenList> {
|
|
50
|
+
return this.#transport.request<TokenList>({
|
|
51
|
+
method: 'GET',
|
|
52
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}/tokens`,
|
|
53
|
+
idempotent: true,
|
|
54
|
+
signal: options.signal,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One key by id; its value is never part of the answer. */
|
|
59
|
+
get(serverId: string, tokenId: string, options: RequestOptions = {}): Promise<Token> {
|
|
60
|
+
return this.#transport.request<Token>({
|
|
61
|
+
method: 'GET',
|
|
62
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}/tokens/${encodeURIComponent(tokenId)}`,
|
|
63
|
+
idempotent: true,
|
|
64
|
+
signal: options.signal,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The key stops working at once. The last live key of a server cannot be
|
|
70
|
+
* revoked (`last_token`), so a server is never locked out.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* await account.servers.tokens.revoke(serverId, tokenId);
|
|
74
|
+
*/
|
|
75
|
+
revoke(serverId: string, tokenId: string, options: RequestOptions = {}): Promise<void> {
|
|
76
|
+
return this.#transport.request<undefined>({
|
|
77
|
+
method: 'DELETE',
|
|
78
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}/tokens/${encodeURIComponent(tokenId)}`,
|
|
79
|
+
idempotent: true,
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The account's servers: each an isolation boundary with its own keys, streams, log, suppressions and webhooks. */
|
|
86
|
+
export class ServersResource {
|
|
87
|
+
readonly #transport: Transport;
|
|
88
|
+
/** The keys of each server. */
|
|
89
|
+
readonly tokens: ServerTokensResource;
|
|
90
|
+
|
|
91
|
+
constructor(transport: Transport) {
|
|
92
|
+
this.#transport = transport;
|
|
93
|
+
this.tokens = new ServerTokensResource(transport);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a server with its default transactional stream and its first
|
|
98
|
+
* key, whose value is answered this once. A taken name is
|
|
99
|
+
* `server_exists`; the account's cap is `server_limit` with `max`.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const server = await account.servers.create({ name: 'Fakturering' });
|
|
103
|
+
* const sendora = new Sendora({ token: server.token.token });
|
|
104
|
+
*/
|
|
105
|
+
create(request: CreateServerRequest, options: RequestOptions = {}): Promise<CreatedServer> {
|
|
106
|
+
return this.#transport.request<CreatedServer>({
|
|
107
|
+
method: 'POST',
|
|
108
|
+
path: '/v1/servers',
|
|
109
|
+
body: request,
|
|
110
|
+
idempotent: false,
|
|
111
|
+
signal: options.signal,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Every server of the account, oldest first. */
|
|
116
|
+
list(options: RequestOptions = {}): Promise<ServerList> {
|
|
117
|
+
return this.#transport.request<ServerList>({
|
|
118
|
+
method: 'GET',
|
|
119
|
+
path: '/v1/servers',
|
|
120
|
+
idempotent: true,
|
|
121
|
+
signal: options.signal,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One server by id. */
|
|
126
|
+
get(serverId: string, options: RequestOptions = {}): Promise<Server> {
|
|
127
|
+
return this.#transport.request<Server>({
|
|
128
|
+
method: 'GET',
|
|
129
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}`,
|
|
130
|
+
idempotent: true,
|
|
131
|
+
signal: options.signal,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Renames the server; the id, the keys and everything it holds stay. A taken name is `server_exists`. */
|
|
136
|
+
update(
|
|
137
|
+
serverId: string,
|
|
138
|
+
request: UpdateServerRequest,
|
|
139
|
+
options: RequestOptions = {},
|
|
140
|
+
): Promise<Server> {
|
|
141
|
+
return this.#transport.request<Server>({
|
|
142
|
+
method: 'PATCH',
|
|
143
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}`,
|
|
144
|
+
body: request,
|
|
145
|
+
idempotent: true,
|
|
146
|
+
signal: options.signal,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Removes the server with its keys, streams, messages, suppressions and
|
|
152
|
+
* webhooks. Refused with `server_in_flight` while its messages are still
|
|
153
|
+
* being delivered; try again once they have left.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* await account.servers.delete(serverId);
|
|
157
|
+
*/
|
|
158
|
+
delete(serverId: string, options: RequestOptions = {}): Promise<void> {
|
|
159
|
+
return this.#transport.request<undefined>({
|
|
160
|
+
method: 'DELETE',
|
|
161
|
+
path: `/v1/servers/${encodeURIComponent(serverId)}`,
|
|
162
|
+
idempotent: true,
|
|
163
|
+
signal: options.signal,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -586,13 +586,13 @@ export interface BroadcastPage {
|
|
|
586
586
|
/** What `broadcasts.list` takes. */
|
|
587
587
|
export type BroadcastList = PageQuery;
|
|
588
588
|
|
|
589
|
-
/** The body POST /v1/tokens
|
|
589
|
+
/** The body POST /v1/tokens and POST /v1/servers/{id}/tokens take. */
|
|
590
590
|
export interface CreateTokenRequest {
|
|
591
591
|
/** 1 to 100 characters, for people to tell tokens apart. */
|
|
592
592
|
name: string;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
-
/** A
|
|
595
|
+
/** A key of a server; its value is shown only when it is created. */
|
|
596
596
|
export interface Token {
|
|
597
597
|
tokenId: string;
|
|
598
598
|
name: string;
|
|
@@ -602,7 +602,7 @@ export interface Token {
|
|
|
602
602
|
revokedAt: string | null;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
/** A
|
|
605
|
+
/** A key just created, with its value, shown this once. */
|
|
606
606
|
export interface CreatedToken extends Token {
|
|
607
607
|
token: string;
|
|
608
608
|
}
|
|
@@ -611,6 +611,37 @@ export interface TokenList {
|
|
|
611
611
|
tokens: Token[];
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
+
/** A server of the account: an isolation boundary with its own keys, streams, log, suppressions and webhooks. */
|
|
615
|
+
export interface Server {
|
|
616
|
+
/** The id every server key, stream and message of the server refers to. */
|
|
617
|
+
serverId: string;
|
|
618
|
+
/** The name given at creation, unique within the account. */
|
|
619
|
+
name: string;
|
|
620
|
+
createdAt: string;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** A server just created, with its first key, whose value is shown this once. */
|
|
624
|
+
export interface CreatedServer extends Server {
|
|
625
|
+
/** The server's first key, named default; its value is never shown again. */
|
|
626
|
+
token: CreatedToken;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export interface ServerList {
|
|
630
|
+
servers: Server[];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** The body POST /v1/servers takes. */
|
|
634
|
+
export interface CreateServerRequest {
|
|
635
|
+
/** 1 to 100 characters, unique within the account. */
|
|
636
|
+
name: string;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** The body PATCH /v1/servers/{id} takes. */
|
|
640
|
+
export interface UpdateServerRequest {
|
|
641
|
+
/** 1 to 100 characters, unique within the account. */
|
|
642
|
+
name: string;
|
|
643
|
+
}
|
|
644
|
+
|
|
614
645
|
/** The events a webhook can receive. */
|
|
615
646
|
export type WebhookEventName =
|
|
616
647
|
| 'delivered'
|
|
@@ -649,6 +680,19 @@ export interface Webhook {
|
|
|
649
680
|
/** How the inbound event carries a received message: the parsed message, or a reference. */
|
|
650
681
|
inboundContent: InboundContent;
|
|
651
682
|
createdAt: string;
|
|
683
|
+
/** The live secrets, oldest first; every delivery is signed with each of them, the newest first in Sendora-Signature. */
|
|
684
|
+
secrets: WebhookSecret[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** A live signing secret of a webhook by id and age; its value was shown once when it was created. */
|
|
688
|
+
export interface WebhookSecret {
|
|
689
|
+
secretId: string;
|
|
690
|
+
createdAt: string;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** A secret just created, with its value, shown this once. */
|
|
694
|
+
export interface CreatedWebhookSecret extends WebhookSecret {
|
|
695
|
+
secret: string;
|
|
652
696
|
}
|
|
653
697
|
|
|
654
698
|
/** A webhook just created, with its signing secret, shown this once. */
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package version, sent as the User-Agent; a test holds it equal to package.json. */
|
|
2
|
-
export const SDK_VERSION = '1.
|
|
2
|
+
export const SDK_VERSION = '2.1.0';
|