@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.
Files changed (54) hide show
  1. package/README.md +87 -26
  2. package/dist/account.d.ts +30 -0
  3. package/dist/account.d.ts.map +1 -0
  4. package/dist/account.js +25 -0
  5. package/dist/account.js.map +1 -0
  6. package/dist/client.d.ts +9 -16
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +5 -30
  9. package/dist/client.js.map +1 -1
  10. package/dist/domains.d.ts +1 -1
  11. package/dist/domains.d.ts.map +1 -1
  12. package/dist/domains.js +1 -1
  13. package/dist/domains.js.map +1 -1
  14. package/dist/error.d.ts +5 -2
  15. package/dist/error.d.ts.map +1 -1
  16. package/dist/error.js +13 -0
  17. package/dist/error.js.map +1 -1
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/options.d.ts +20 -0
  23. package/dist/options.d.ts.map +1 -0
  24. package/dist/options.js +34 -0
  25. package/dist/options.js.map +1 -0
  26. package/dist/servers.d.ts +64 -0
  27. package/dist/servers.d.ts.map +1 -0
  28. package/dist/servers.js +133 -0
  29. package/dist/servers.js.map +1 -0
  30. package/dist/types.d.ts +40 -3
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/dist/webhook-verify.d.ts +7 -5
  35. package/dist/webhook-verify.d.ts.map +1 -1
  36. package/dist/webhook-verify.js +38 -17
  37. package/dist/webhook-verify.js.map +1 -1
  38. package/dist/webhooks.d.ts +13 -1
  39. package/dist/webhooks.d.ts.map +1 -1
  40. package/dist/webhooks.js +26 -0
  41. package/dist/webhooks.js.map +1 -1
  42. package/package.json +4 -4
  43. package/skills/sendora/SKILL.md +19 -9
  44. package/src/account.ts +40 -0
  45. package/src/client.ts +14 -43
  46. package/src/domains.ts +1 -1
  47. package/src/error.ts +23 -1
  48. package/src/index.ts +6 -1
  49. package/src/options.ts +51 -0
  50. package/src/servers.ts +166 -0
  51. package/src/types.ts +47 -3
  52. package/src/version.ts +1 -1
  53. package/src/webhook-verify.ts +39 -18
  54. 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`,
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
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.2.0",
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/db": "0.0.0",
37
- "@sendora/domain": "0.0.0",
36
+ "@sendora/observability": "0.0.0",
38
37
  "@sendora/api": "0.0.0",
39
- "@sendora/observability": "0.0.0"
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"
@@ -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, tokens, webhooks and sending domains, and receive signed webhooks. Use when a task mentions Sendora, @sendora/sdk, or sending email from a system hosted in Sweden.
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
- - The API token is a secret. Read it from the environment on the server
16
- and never send it to a browser. `new Sendora({ token })` throws when the
17
- token is missing, so construct the client at startup.
18
- - One client per server token: `const sendora = new Sendora({ token })`.
19
- Every resource hangs off it: `email`, `messages`, `streams`,
20
- `suppressions`, `tokens`, `webhooks`, `domains`, `inboundDomains`.
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; `domains.create` answers
44
- the two DNS records to add, and `domains.verify` checks them now.
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 { DEFAULT_MAX_RETRIES } from './retry.ts';
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 const DEFAULT_BASE_URL = 'https://api.sendora.se';
16
- const DEFAULT_TIMEOUT_MS = 30_000;
17
-
18
- export interface SendoraOptions {
12
+ export interface SendoraOptions extends ClientOptions {
19
13
  /**
20
- * A server's API token, created in the dashboard. Keep it on the server;
21
- * never ship it to a browser. `undefined`, as an unset environment
22
- * variable gives, throws at construction rather than at the first call.
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 token; every resource hangs off it.
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
- if (options.token === undefined || options.token.trim() === '') {
63
- throw new TypeError('Sendora needs a server API token; create one in the dashboard.');
64
- }
65
- const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
66
- new URL(baseUrl);
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. Any server's token manages them. */
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 { DEFAULT_BASE_URL, Sendora } from './client.ts';
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 takes. */
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 token of the server; its value is shown only when it is created. */
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 token just created, with its value, shown this once. */
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.0';
2
+ export const SDK_VERSION = '2.1.0';