@sendora/sdk 1.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 (93) hide show
  1. package/README.md +364 -0
  2. package/dist/base64.d.ts +3 -0
  3. package/dist/base64.d.ts.map +1 -0
  4. package/dist/base64.js +17 -0
  5. package/dist/base64.js.map +1 -0
  6. package/dist/broadcasts.d.ts +60 -0
  7. package/dist/broadcasts.d.ts.map +1 -0
  8. package/dist/broadcasts.js +98 -0
  9. package/dist/broadcasts.js.map +1 -0
  10. package/dist/client.d.ts +51 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +69 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/domains.d.ts +34 -0
  15. package/dist/domains.d.ts.map +1 -0
  16. package/dist/domains.js +70 -0
  17. package/dist/domains.js.map +1 -0
  18. package/dist/email.d.ts +40 -0
  19. package/dist/email.d.ts.map +1 -0
  20. package/dist/email.js +68 -0
  21. package/dist/email.js.map +1 -0
  22. package/dist/error.d.ts +67 -0
  23. package/dist/error.d.ts.map +1 -0
  24. package/dist/error.js +173 -0
  25. package/dist/error.js.map +1 -0
  26. package/dist/index.d.ts +17 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/messages.d.ts +37 -0
  31. package/dist/messages.d.ts.map +1 -0
  32. package/dist/messages.js +68 -0
  33. package/dist/messages.js.map +1 -0
  34. package/dist/pagination.d.ts +3 -0
  35. package/dist/pagination.d.ts.map +1 -0
  36. package/dist/pagination.js +14 -0
  37. package/dist/pagination.js.map +1 -0
  38. package/dist/retry.d.ts +6 -0
  39. package/dist/retry.d.ts.map +1 -0
  40. package/dist/retry.js +14 -0
  41. package/dist/retry.js.map +1 -0
  42. package/dist/streams.d.ts +45 -0
  43. package/dist/streams.d.ts.map +1 -0
  44. package/dist/streams.js +82 -0
  45. package/dist/streams.js.map +1 -0
  46. package/dist/suppressions.d.ts +37 -0
  47. package/dist/suppressions.d.ts.map +1 -0
  48. package/dist/suppressions.js +56 -0
  49. package/dist/suppressions.js.map +1 -0
  50. package/dist/tokens.d.ts +28 -0
  51. package/dist/tokens.d.ts.map +1 -0
  52. package/dist/tokens.js +57 -0
  53. package/dist/tokens.js.map +1 -0
  54. package/dist/transport.d.ts +34 -0
  55. package/dist/transport.d.ts.map +1 -0
  56. package/dist/transport.js +146 -0
  57. package/dist/transport.js.map +1 -0
  58. package/dist/types.d.ts +802 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/version.d.ts +3 -0
  63. package/dist/version.d.ts.map +1 -0
  64. package/dist/version.js +3 -0
  65. package/dist/version.js.map +1 -0
  66. package/dist/webhook-verify.d.ts +33 -0
  67. package/dist/webhook-verify.d.ts.map +1 -0
  68. package/dist/webhook-verify.js +115 -0
  69. package/dist/webhook-verify.js.map +1 -0
  70. package/dist/webhooks.d.ts +63 -0
  71. package/dist/webhooks.d.ts.map +1 -0
  72. package/dist/webhooks.js +118 -0
  73. package/dist/webhooks.js.map +1 -0
  74. package/package.json +49 -0
  75. package/skills/sendora/SKILL.md +86 -0
  76. package/src/base64.ts +17 -0
  77. package/src/broadcasts.ts +115 -0
  78. package/src/client.ts +88 -0
  79. package/src/domains.ts +84 -0
  80. package/src/email.ts +83 -0
  81. package/src/error.ts +252 -0
  82. package/src/index.ts +16 -0
  83. package/src/messages.ts +88 -0
  84. package/src/pagination.ts +18 -0
  85. package/src/retry.ts +20 -0
  86. package/src/streams.ts +100 -0
  87. package/src/suppressions.ts +73 -0
  88. package/src/tokens.ts +70 -0
  89. package/src/transport.ts +179 -0
  90. package/src/types.ts +848 -0
  91. package/src/version.ts +2 -0
  92. package/src/webhook-verify.ts +147 -0
  93. package/src/webhooks.ts +157 -0
@@ -0,0 +1,68 @@
1
+ import { paginate } from "./pagination.js";
2
+ /** The message log of the token's server and each message's timeline. */
3
+ export class MessagesResource {
4
+ #transport;
5
+ constructor(transport) {
6
+ this.#transport = transport;
7
+ }
8
+ /**
9
+ * One message as the log holds it: its recipients with their state, its
10
+ * attachments described, and every event on its timeline. Bodies are
11
+ * never returned.
12
+ *
13
+ * @example
14
+ * const message = await sendora.messages.get(messageId);
15
+ * const delivered = message.recipients.every((recipient) => recipient.status === 'delivered');
16
+ */
17
+ get(messageId, options = {}) {
18
+ return this.#transport.request({
19
+ method: 'GET',
20
+ path: `/v1/messages/${encodeURIComponent(messageId)}`,
21
+ idempotent: true,
22
+ signal: options.signal,
23
+ });
24
+ }
25
+ /**
26
+ * One page of the log, newest first. Every filter is optional; pass the
27
+ * page's `next` as `after` for the following page, or use `searchAll`.
28
+ *
29
+ * @example
30
+ * const page = await sendora.messages.search({ tag: 'invoice', status: 'bounced', limit: 20 });
31
+ */
32
+ search(search = {}, options = {}) {
33
+ return this.#transport.request({
34
+ method: 'POST',
35
+ path: '/v1/messages/search',
36
+ body: encodeSearch(search),
37
+ idempotent: true,
38
+ signal: options.signal,
39
+ });
40
+ }
41
+ /**
42
+ * Every message the filters match, page by page, for `for await`.
43
+ *
44
+ * @example
45
+ * for await (const message of sendora.messages.searchAll({ recipient: 'anna@example.com' })) {
46
+ * console.log(message.messageId, message.subject);
47
+ * }
48
+ */
49
+ searchAll(search = {}, options = {}) {
50
+ return paginate((after) => this.search({ ...search, after }, options), (page) => page.messages, (page) => page.next, search.after);
51
+ }
52
+ }
53
+ /** The body as the API takes it: times as ISO 8601 strings. */
54
+ export function encodeSearch(search) {
55
+ const { from, to, ...fields } = search;
56
+ const body = { ...fields };
57
+ if (from !== undefined) {
58
+ body.from = isoOf(from);
59
+ }
60
+ if (to !== undefined) {
61
+ body.to = isoOf(to);
62
+ }
63
+ return body;
64
+ }
65
+ function isoOf(value) {
66
+ return typeof value === 'string' ? value : value.toISOString();
67
+ }
68
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAW3C,yEAAyE;AACzE,MAAM,OAAO,gBAAgB;IAClB,UAAU,CAAY;IAE/B,YAAY,SAAoB;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED;;;;;;;;OAQG;IACH,GAAG,CAAC,SAAiB,EAAE,UAA0B,EAAE;QACjD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAgB;YAC5C,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,gBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE;YACrD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,SAAwB,EAAE,EAAE,UAA0B,EAAE;QAC7D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAc;YAC1C,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,qBAAqB;YAC3B,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC;YAC1B,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,SAAwB,EAAE,EAAE,UAA0B,EAAE;QAChE,OAAO,QAAQ,CACb,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,EACrD,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EACvB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EACnB,MAAM,CAAC,KAAK,CACb,CAAC;IACJ,CAAC;CACF;AAED,+DAA+D;AAC/D,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC;IACvC,MAAM,IAAI,GAAsB,EAAE,GAAG,MAAM,EAAE,CAAC;IAC9C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IACD,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,KAAK,CAAC,KAAoB;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Walks a cursor-paged list: fetches a page, yields its items, follows `next` until it is null. */
2
+ export declare function paginate<Page, Item>(fetchPage: (after: string | undefined) => Promise<Page>, itemsOf: (page: Page) => Item[], nextOf: (page: Page) => string | null, start: string | undefined): AsyncGenerator<Item, void, undefined>;
3
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,wBAAuB,QAAQ,CAAC,IAAI,EAAE,IAAI,EACxC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,EACvD,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,EAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,GAAG,IAAI,EACrC,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAWvC"}
@@ -0,0 +1,14 @@
1
+ /** Walks a cursor-paged list: fetches a page, yields its items, follows `next` until it is null. */
2
+ export async function* paginate(fetchPage, itemsOf, nextOf, start) {
3
+ let after = start;
4
+ for (;;) {
5
+ const page = await fetchPage(after);
6
+ yield* itemsOf(page);
7
+ const next = nextOf(page);
8
+ if (next === null) {
9
+ return;
10
+ }
11
+ after = next;
12
+ }
13
+ }
14
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,QAAQ,CAC7B,SAAuD,EACvD,OAA+B,EAC/B,MAAqC,EACrC,KAAyB;IAEzB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QACpC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QACD,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare const DEFAULT_MAX_RETRIES = 2;
2
+ /** The most a call waits across all its retries; a longer Retry-After is handed to the caller instead. */
3
+ export declare const MAX_TOTAL_WAIT_MS = 5000;
4
+ /** Retry-After in full when the API gave one; otherwise 500 ms doubling to 2 s, jittered between half and one and a half. */
5
+ export declare function retryDelayMs(attempt: number, retryAfterSeconds: number | null, random: () => number): number;
6
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,0GAA0G;AAC1G,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAKtC,6HAA6H;AAC7H,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,MAAM,EAAE,MAAM,MAAM,GACnB,MAAM,CAMR"}
package/dist/retry.js ADDED
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_MAX_RETRIES = 2;
2
+ /** The most a call waits across all its retries; a longer Retry-After is handed to the caller instead. */
3
+ export const MAX_TOTAL_WAIT_MS = 5000;
4
+ const BASE_DELAY_MS = 500;
5
+ const MAX_BACKOFF_MS = 2000;
6
+ /** Retry-After in full when the API gave one; otherwise 500 ms doubling to 2 s, jittered between half and one and a half. */
7
+ export function retryDelayMs(attempt, retryAfterSeconds, random) {
8
+ if (retryAfterSeconds !== null) {
9
+ return retryAfterSeconds * 1000;
10
+ }
11
+ const backoff = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_BACKOFF_MS);
12
+ return Math.round(backoff * (0.5 + random()));
13
+ }
14
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,0GAA0G;AAC1G,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,6HAA6H;AAC7H,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,iBAAgC,EAChC,MAAoB;IAEpB,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,OAAO,iBAAiB,GAAG,IAAI,CAAC;IAClC,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,OAAO,EAAE,cAAc,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,GAAG,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { Transport } from './transport.ts';
2
+ import type { CreateStreamRequest, RequestOptions, Stream, StreamList, UpdateStreamRequest } from './types.ts';
3
+ /** The streams of the token's own server: the default transactional one, and any the account added. */
4
+ export declare class StreamsResource {
5
+ #private;
6
+ constructor(transport: Transport);
7
+ /**
8
+ * Adds a stream to the server. A transactional stream is available to
9
+ * every account; a broadcast stream needs broadcast enabled for the
10
+ * account (`broadcast_not_enabled`); an inbound stream receives mail at
11
+ * its own address, one live per server (`inbound_stream_exists`), on an
12
+ * active account (`tenant_not_active`). A taken name answers
13
+ * `stream_exists` with `existingId`.
14
+ *
15
+ * @example
16
+ * const stream = await sendora.streams.create({ kind: 'transactional', name: 'Aviseringar' });
17
+ * await sendora.email.send({ ...message, streamId: stream.streamId });
18
+ */
19
+ create(request: CreateStreamRequest, options?: RequestOptions): Promise<Stream>;
20
+ /** Every stream of the server, archived ones included, oldest first. */
21
+ list(options?: RequestOptions): Promise<StreamList>;
22
+ /** One stream by id. */
23
+ get(streamId: string, options?: RequestOptions): Promise<Stream>;
24
+ /**
25
+ * Renames a stream, or sets how long an inbound stream keeps received
26
+ * content; the id stays. A name another stream of the server carries
27
+ * answers `stream_exists`.
28
+ *
29
+ * @example
30
+ * await sendora.streams.update(streamId, { name: 'Driftinformation' });
31
+ * await sendora.streams.update(inboundStreamId, { contentRetentionDays: 7 });
32
+ */
33
+ update(streamId: string, request: UpdateStreamRequest, options?: RequestOptions): Promise<Stream>;
34
+ /**
35
+ * The stream takes no new messages from now on; what it already holds
36
+ * is still delivered. Archiving again changes nothing. The default
37
+ * stream cannot be archived (`default_stream`).
38
+ *
39
+ * @example
40
+ * const archived = await sendora.streams.archive(streamId);
41
+ * console.log(archived.archivedAt);
42
+ */
43
+ archive(streamId: string, options?: RequestOptions): Promise<Stream>;
44
+ }
45
+ //# sourceMappingURL=streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,MAAM,EACN,UAAU,EACV,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB,uGAAuG;AACvG,qBAAa,eAAe;;gBAGd,SAAS,EAAE,SAAS;IAIhC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAUnF,wEAAwE;IACxE,IAAI,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IASvD,wBAAwB;IACxB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IASpE;;;;;;;;OAQG;IACH,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC;IAUlB;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;CAQzE"}
@@ -0,0 +1,82 @@
1
+ /** The streams of the token's own server: the default transactional one, and any the account added. */
2
+ export class StreamsResource {
3
+ #transport;
4
+ constructor(transport) {
5
+ this.#transport = transport;
6
+ }
7
+ /**
8
+ * Adds a stream to the server. A transactional stream is available to
9
+ * every account; a broadcast stream needs broadcast enabled for the
10
+ * account (`broadcast_not_enabled`); an inbound stream receives mail at
11
+ * its own address, one live per server (`inbound_stream_exists`), on an
12
+ * active account (`tenant_not_active`). A taken name answers
13
+ * `stream_exists` with `existingId`.
14
+ *
15
+ * @example
16
+ * const stream = await sendora.streams.create({ kind: 'transactional', name: 'Aviseringar' });
17
+ * await sendora.email.send({ ...message, streamId: stream.streamId });
18
+ */
19
+ create(request, options = {}) {
20
+ return this.#transport.request({
21
+ method: 'POST',
22
+ path: '/v1/streams',
23
+ body: request,
24
+ idempotent: false,
25
+ signal: options.signal,
26
+ });
27
+ }
28
+ /** Every stream of the server, archived ones included, oldest first. */
29
+ list(options = {}) {
30
+ return this.#transport.request({
31
+ method: 'GET',
32
+ path: '/v1/streams',
33
+ idempotent: true,
34
+ signal: options.signal,
35
+ });
36
+ }
37
+ /** One stream by id. */
38
+ get(streamId, options = {}) {
39
+ return this.#transport.request({
40
+ method: 'GET',
41
+ path: `/v1/streams/${encodeURIComponent(streamId)}`,
42
+ idempotent: true,
43
+ signal: options.signal,
44
+ });
45
+ }
46
+ /**
47
+ * Renames a stream, or sets how long an inbound stream keeps received
48
+ * content; the id stays. A name another stream of the server carries
49
+ * answers `stream_exists`.
50
+ *
51
+ * @example
52
+ * await sendora.streams.update(streamId, { name: 'Driftinformation' });
53
+ * await sendora.streams.update(inboundStreamId, { contentRetentionDays: 7 });
54
+ */
55
+ update(streamId, request, options = {}) {
56
+ return this.#transport.request({
57
+ method: 'PATCH',
58
+ path: `/v1/streams/${encodeURIComponent(streamId)}`,
59
+ body: request,
60
+ idempotent: true,
61
+ signal: options.signal,
62
+ });
63
+ }
64
+ /**
65
+ * The stream takes no new messages from now on; what it already holds
66
+ * is still delivered. Archiving again changes nothing. The default
67
+ * stream cannot be archived (`default_stream`).
68
+ *
69
+ * @example
70
+ * const archived = await sendora.streams.archive(streamId);
71
+ * console.log(archived.archivedAt);
72
+ */
73
+ archive(streamId, options = {}) {
74
+ return this.#transport.request({
75
+ method: 'POST',
76
+ path: `/v1/streams/${encodeURIComponent(streamId)}/archive`,
77
+ idempotent: true,
78
+ signal: options.signal,
79
+ });
80
+ }
81
+ }
82
+ //# sourceMappingURL=streams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.js","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"AASA,uGAAuG;AACvG,MAAM,OAAO,eAAe;IACjB,UAAU,CAAY;IAE/B,YAAY,SAAoB;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAA4B,EAAE,UAA0B,EAAE;QAC/D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAS;YACrC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED,wEAAwE;IACxE,IAAI,CAAC,UAA0B,EAAE;QAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAa;YACzC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,aAAa;YACnB,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED,wBAAwB;IACxB,GAAG,CAAC,QAAgB,EAAE,UAA0B,EAAE;QAChD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAS;YACrC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,eAAe,kBAAkB,CAAC,QAAQ,CAAC,EAAE;YACnD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CACJ,QAAgB,EAChB,OAA4B,EAC5B,UAA0B,EAAE;QAE5B,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAS;YACrC,MAAM,EAAE,OAAO;YACf,IAAI,EAAE,eAAe,kBAAkB,CAAC,QAAQ,CAAC,EAAE;YACnD,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAgB,EAAE,UAA0B,EAAE;QACpD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAS;YACrC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,eAAe,kBAAkB,CAAC,QAAQ,CAAC,UAAU;YAC3D,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ import type { Transport } from './transport.ts';
2
+ import type { DeleteSuppressionRequest, RequestOptions, Suppression, SuppressionPage, SuppressionQuery } from './types.ts';
3
+ /** The addresses the token's server no longer sends to, one list per stream. */
4
+ export declare class SuppressionsResource {
5
+ #private;
6
+ constructor(transport: Transport);
7
+ /**
8
+ * One page of a stream's list, newest first: hard bounces, spam
9
+ * complaints and addresses added by hand. The default stream's list
10
+ * unless `streamId` names another. A send to any of them is refused
11
+ * with `recipient_suppressed`.
12
+ *
13
+ * @example
14
+ * const { suppressions, next } = await sendora.suppressions.list({ streamId, limit: 200 });
15
+ */
16
+ list(query?: SuppressionQuery, options?: RequestOptions): Promise<SuppressionPage>;
17
+ /**
18
+ * Every suppressed address, page by page, for `for await`.
19
+ *
20
+ * @example
21
+ * for await (const entry of sendora.suppressions.listAll()) {
22
+ * console.log(entry.address, entry.reason);
23
+ * }
24
+ */
25
+ listAll(query?: SuppressionQuery, options?: RequestOptions): AsyncIterable<Suppression>;
26
+ /**
27
+ * Lifts a hard bounce or a manual entry from a stream's list so the
28
+ * server may send to the address again on that stream; the default
29
+ * stream's list unless `streamId` names another. A spam complaint cannot
30
+ * be lifted this way: `spam_complaint_locked`.
31
+ *
32
+ * @example
33
+ * await sendora.suppressions.delete({ address: 'anna@example.com' });
34
+ */
35
+ delete(request: DeleteSuppressionRequest, options?: RequestOptions): Promise<void>;
36
+ }
37
+ //# sourceMappingURL=suppressions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suppressions.d.ts","sourceRoot":"","sources":["../src/suppressions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,gFAAgF;AAChF,qBAAa,oBAAoB;;gBAGnB,SAAS,EAAE,SAAS;IAIhC;;;;;;;;OAQG;IACH,IAAI,CAAC,KAAK,GAAE,gBAAqB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAU1F;;;;;;;OAOG;IACH,OAAO,CAAC,KAAK,GAAE,gBAAqB,EAAE,OAAO,GAAE,cAAmB,GAAG,aAAa,CAAC,WAAW,CAAC;IAS/F;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,wBAAwB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CASvF"}
@@ -0,0 +1,56 @@
1
+ import { paginate } from "./pagination.js";
2
+ /** The addresses the token's server no longer sends to, one list per stream. */
3
+ export class SuppressionsResource {
4
+ #transport;
5
+ constructor(transport) {
6
+ this.#transport = transport;
7
+ }
8
+ /**
9
+ * One page of a stream's list, newest first: hard bounces, spam
10
+ * complaints and addresses added by hand. The default stream's list
11
+ * unless `streamId` names another. A send to any of them is refused
12
+ * with `recipient_suppressed`.
13
+ *
14
+ * @example
15
+ * const { suppressions, next } = await sendora.suppressions.list({ streamId, limit: 200 });
16
+ */
17
+ list(query = {}, options = {}) {
18
+ return this.#transport.request({
19
+ method: 'GET',
20
+ path: '/v1/suppressions',
21
+ query: { streamId: query.streamId, limit: query.limit, after: query.after },
22
+ idempotent: true,
23
+ signal: options.signal,
24
+ });
25
+ }
26
+ /**
27
+ * Every suppressed address, page by page, for `for await`.
28
+ *
29
+ * @example
30
+ * for await (const entry of sendora.suppressions.listAll()) {
31
+ * console.log(entry.address, entry.reason);
32
+ * }
33
+ */
34
+ listAll(query = {}, options = {}) {
35
+ return paginate((after) => this.list({ ...query, after }, options), (page) => page.suppressions, (page) => page.next, query.after);
36
+ }
37
+ /**
38
+ * Lifts a hard bounce or a manual entry from a stream's list so the
39
+ * server may send to the address again on that stream; the default
40
+ * stream's list unless `streamId` names another. A spam complaint cannot
41
+ * be lifted this way: `spam_complaint_locked`.
42
+ *
43
+ * @example
44
+ * await sendora.suppressions.delete({ address: 'anna@example.com' });
45
+ */
46
+ delete(request, options = {}) {
47
+ return this.#transport.request({
48
+ method: 'POST',
49
+ path: '/v1/suppressions/delete',
50
+ body: request,
51
+ idempotent: true,
52
+ signal: options.signal,
53
+ });
54
+ }
55
+ }
56
+ //# sourceMappingURL=suppressions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suppressions.js","sourceRoot":"","sources":["../src/suppressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAU3C,gFAAgF;AAChF,MAAM,OAAO,oBAAoB;IACtB,UAAU,CAAY;IAE/B,YAAY,SAAoB;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,CAAC,QAA0B,EAAE,EAAE,UAA0B,EAAE;QAC7D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAkB;YAC9C,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE;YAC3E,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,QAA0B,EAAE,EAAE,UAA0B,EAAE;QAChE,OAAO,QAAQ,CACb,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,EAClD,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,EAC3B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EACnB,KAAK,CAAC,KAAK,CACZ,CAAC;IACJ,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAiC,EAAE,UAA0B,EAAE;QACpE,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAY;YACxC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,yBAAyB;YAC/B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import type { Transport } from './transport.ts';
2
+ import type { CreatedToken, CreateTokenRequest, RequestOptions, Token, TokenList } from './types.ts';
3
+ /** The API tokens of the token's own server. Every token of a server has the same rights. */
4
+ export declare class TokensResource {
5
+ #private;
6
+ constructor(transport: Transport);
7
+ /**
8
+ * Creates another live token and answers its value once; it is never
9
+ * shown again, so store it at once.
10
+ *
11
+ * @example
12
+ * const { token, tokenId } = await sendora.tokens.create({ name: 'Fakturasystemet' });
13
+ */
14
+ create(request: CreateTokenRequest, options?: RequestOptions): Promise<CreatedToken>;
15
+ /** Every token of the server, revoked ones included. */
16
+ list(options?: RequestOptions): Promise<TokenList>;
17
+ /** One token by id; its value is never part of the answer. */
18
+ get(tokenId: string, options?: RequestOptions): Promise<Token>;
19
+ /**
20
+ * The token stops working at once. The last live token of a server
21
+ * cannot be revoked (`last_token`), so a server is never locked out.
22
+ *
23
+ * @example
24
+ * await sendora.tokens.revoke(tokenId);
25
+ */
26
+ revoke(tokenId: string, options?: RequestOptions): Promise<void>;
27
+ }
28
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,KAAK,EACL,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,6FAA6F;AAC7F,qBAAa,cAAc;;gBAGb,SAAS,EAAE,SAAS;IAIhC;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,YAAY,CAAC;IAUxF,wDAAwD;IACxD,IAAI,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,SAAS,CAAC;IAStD,8DAA8D;IAC9D,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,KAAK,CAAC;IASlE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAQrE"}
package/dist/tokens.js ADDED
@@ -0,0 +1,57 @@
1
+ /** The API tokens of the token's own server. Every token of a server has the same rights. */
2
+ export class TokensResource {
3
+ #transport;
4
+ constructor(transport) {
5
+ this.#transport = transport;
6
+ }
7
+ /**
8
+ * Creates another live token and answers its value once; it is never
9
+ * shown again, so store it at once.
10
+ *
11
+ * @example
12
+ * const { token, tokenId } = await sendora.tokens.create({ name: 'Fakturasystemet' });
13
+ */
14
+ create(request, options = {}) {
15
+ return this.#transport.request({
16
+ method: 'POST',
17
+ path: '/v1/tokens',
18
+ body: request,
19
+ idempotent: false,
20
+ signal: options.signal,
21
+ });
22
+ }
23
+ /** Every token of the server, revoked ones included. */
24
+ list(options = {}) {
25
+ return this.#transport.request({
26
+ method: 'GET',
27
+ path: '/v1/tokens',
28
+ idempotent: true,
29
+ signal: options.signal,
30
+ });
31
+ }
32
+ /** One token by id; its value is never part of the answer. */
33
+ get(tokenId, options = {}) {
34
+ return this.#transport.request({
35
+ method: 'GET',
36
+ path: `/v1/tokens/${encodeURIComponent(tokenId)}`,
37
+ idempotent: true,
38
+ signal: options.signal,
39
+ });
40
+ }
41
+ /**
42
+ * The token stops working at once. The last live token of a server
43
+ * cannot be revoked (`last_token`), so a server is never locked out.
44
+ *
45
+ * @example
46
+ * await sendora.tokens.revoke(tokenId);
47
+ */
48
+ revoke(tokenId, options = {}) {
49
+ return this.#transport.request({
50
+ method: 'DELETE',
51
+ path: `/v1/tokens/${encodeURIComponent(tokenId)}`,
52
+ idempotent: true,
53
+ signal: options.signal,
54
+ });
55
+ }
56
+ }
57
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AASA,6FAA6F;AAC7F,MAAM,OAAO,cAAc;IAChB,UAAU,CAAY;IAE/B,YAAY,SAAoB;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,OAA2B,EAAE,UAA0B,EAAE;QAC9D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAe;YAC3C,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED,wDAAwD;IACxD,IAAI,CAAC,UAA0B,EAAE;QAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAY;YACxC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED,8DAA8D;IAC9D,GAAG,CAAC,OAAe,EAAE,UAA0B,EAAE;QAC/C,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAQ;YACpC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,cAAc,kBAAkB,CAAC,OAAO,CAAC,EAAE;YACjD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,OAAe,EAAE,UAA0B,EAAE;QAClD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAY;YACxC,MAAM,EAAE,QAAQ;YAChB,IAAI,EAAE,cAAc,kBAAkB,CAAC,OAAO,CAAC,EAAE;YACjD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,34 @@
1
+ export type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
2
+ export interface RequestSpec {
3
+ method: Method;
4
+ path: string;
5
+ query?: Record<string, string | number | undefined> | undefined;
6
+ body?: unknown;
7
+ headers?: Record<string, string> | undefined;
8
+ /** Safe to send again after a lost connection: reads, deletes and sends under an idempotency key. */
9
+ idempotent: boolean;
10
+ signal?: AbortSignal | undefined;
11
+ }
12
+ export interface TransportOptions {
13
+ baseUrl: string;
14
+ token: string;
15
+ fetch: typeof fetch;
16
+ timeoutMs: number;
17
+ maxRetries: number;
18
+ userAgent: string;
19
+ /** Waits between attempts; tests replace it. */
20
+ sleep?: ((ms: number) => Promise<void>) | undefined;
21
+ /** The jitter source in [0, 1); tests pin it. */
22
+ random?: (() => number) | undefined;
23
+ }
24
+ /**
25
+ * One request path for every resource: builds the request, sends it,
26
+ * turns the answer into a value or a SendoraError, and repeats it within
27
+ * the retry budget when repeating could help and cannot cause harm.
28
+ */
29
+ export declare class Transport {
30
+ #private;
31
+ constructor(options: TransportOptions);
32
+ request<T>(spec: RequestSpec): Promise<T>;
33
+ }
34
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7C,qGAAqG;IACrG,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IACpD,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CACrC;AAID;;;;GAIG;AACH,qBAAa,SAAS;;gBAKR,OAAO,EAAE,gBAAgB;IAM/B,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAwHhD"}
@@ -0,0 +1,146 @@
1
+ import { errorFromAnswer, SendoraError } from "./error.js";
2
+ import { MAX_TOTAL_WAIT_MS, retryDelayMs } from "./retry.js";
3
+ /**
4
+ * One request path for every resource: builds the request, sends it,
5
+ * turns the answer into a value or a SendoraError, and repeats it within
6
+ * the retry budget when repeating could help and cannot cause harm.
7
+ */
8
+ export class Transport {
9
+ #options;
10
+ #sleep;
11
+ #random;
12
+ constructor(options) {
13
+ this.#options = options;
14
+ this.#sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
15
+ this.#random = options.random ?? Math.random;
16
+ }
17
+ async request(spec) {
18
+ const url = this.#url(spec);
19
+ const body = spec.body === undefined ? undefined : JSON.stringify(spec.body);
20
+ let waitedMs = 0;
21
+ for (let attempt = 0;; attempt += 1) {
22
+ const outcome = await this.#attempt(spec, url, body);
23
+ if (outcome.ok) {
24
+ return outcome.value;
25
+ }
26
+ const { error } = outcome;
27
+ const mayRepeat = spec.idempotent || error.code === 'rate_limited' || error.code === 'sending_disabled';
28
+ if (attempt >= this.#options.maxRetries || !error.retryable || !mayRepeat) {
29
+ throw error;
30
+ }
31
+ const delay = retryDelayMs(attempt, error.retryAfter, this.#random);
32
+ if (waitedMs + delay > MAX_TOTAL_WAIT_MS) {
33
+ throw error;
34
+ }
35
+ waitedMs += delay;
36
+ await this.#sleep(delay);
37
+ }
38
+ }
39
+ async #attempt(spec, url, body) {
40
+ const timeout = AbortSignal.timeout(this.#options.timeoutMs);
41
+ const signal = spec.signal === undefined ? timeout : AbortSignal.any([spec.signal, timeout]);
42
+ const init = {
43
+ method: spec.method,
44
+ headers: this.#headers(spec, body),
45
+ signal,
46
+ redirect: 'manual',
47
+ };
48
+ if (body !== undefined) {
49
+ init.body = body;
50
+ }
51
+ let response;
52
+ try {
53
+ response = await this.#options.fetch(url, init);
54
+ }
55
+ catch (cause) {
56
+ if (spec.signal?.aborted) {
57
+ throw cause;
58
+ }
59
+ if (timeout.aborted) {
60
+ return {
61
+ ok: false,
62
+ error: new SendoraError({
63
+ code: 'timeout',
64
+ status: null,
65
+ message: `The API did not answer within ${String(this.#options.timeoutMs)} ms.`,
66
+ cause,
67
+ }),
68
+ };
69
+ }
70
+ return {
71
+ ok: false,
72
+ error: new SendoraError({
73
+ code: 'connection_failed',
74
+ status: null,
75
+ message: 'The connection to the API failed.',
76
+ cause,
77
+ }),
78
+ };
79
+ }
80
+ if (response.status === 204) {
81
+ await response.body?.cancel();
82
+ return { ok: true, value: undefined };
83
+ }
84
+ const json = parseJson(await response.text());
85
+ if (response.status === 0 || (response.status >= 300 && response.status < 400)) {
86
+ return {
87
+ ok: false,
88
+ error: new SendoraError({
89
+ code: 'unexpected_response',
90
+ status: response.status,
91
+ message: `The API answered a redirect; check baseUrl (${this.#options.baseUrl}).`,
92
+ }),
93
+ };
94
+ }
95
+ if (response.ok) {
96
+ if (json === undefined) {
97
+ return {
98
+ ok: false,
99
+ error: new SendoraError({
100
+ code: 'unexpected_response',
101
+ status: response.status,
102
+ message: `The API answered ${String(response.status)} without a JSON body.`,
103
+ }),
104
+ };
105
+ }
106
+ return { ok: true, value: json };
107
+ }
108
+ return {
109
+ ok: false,
110
+ error: errorFromAnswer(response.status, json, response.headers.get('retry-after')),
111
+ };
112
+ }
113
+ #headers(spec, body) {
114
+ const headers = {
115
+ authorization: `Bearer ${this.#options.token}`,
116
+ accept: 'application/json',
117
+ 'user-agent': this.#options.userAgent,
118
+ ...spec.headers,
119
+ };
120
+ if (body !== undefined) {
121
+ headers['content-type'] = 'application/json';
122
+ }
123
+ return headers;
124
+ }
125
+ #url(spec) {
126
+ const url = new URL(`${this.#options.baseUrl.replace(/\/+$/, '')}${spec.path}`);
127
+ for (const [name, value] of Object.entries(spec.query ?? {})) {
128
+ if (value !== undefined) {
129
+ url.searchParams.set(name, String(value));
130
+ }
131
+ }
132
+ return url.toString();
133
+ }
134
+ }
135
+ function parseJson(text) {
136
+ if (text === '') {
137
+ return undefined;
138
+ }
139
+ try {
140
+ return JSON.parse(text);
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ }
146
+ //# sourceMappingURL=transport.js.map