@sendora/sdk 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -70
- package/dist/account.d.ts +30 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +25 -0
- package/dist/account.js.map +1 -0
- package/dist/client.d.ts +13 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -30
- package/dist/client.js.map +1 -1
- package/dist/domains.d.ts +1 -1
- package/dist/domains.d.ts.map +1 -1
- package/dist/domains.js +1 -1
- package/dist/domains.js.map +1 -1
- package/dist/error.d.ts +6 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +22 -2
- package/dist/error.js.map +1 -1
- package/dist/inbound-domains.d.ts +36 -0
- package/dist/inbound-domains.d.ts.map +1 -0
- package/dist/inbound-domains.js +72 -0
- package/dist/inbound-domains.js.map +1 -0
- package/dist/inbound.d.ts +61 -0
- package/dist/inbound.d.ts.map +1 -0
- package/dist/inbound.js +106 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +20 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +34 -0
- package/dist/options.js.map +1 -0
- package/dist/servers.d.ts +64 -0
- package/dist/servers.d.ts.map +1 -0
- package/dist/servers.js +133 -0
- package/dist/servers.js.map +1 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +15 -5
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +182 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook-verify.d.ts +7 -5
- package/dist/webhook-verify.d.ts.map +1 -1
- package/dist/webhook-verify.js +38 -17
- package/dist/webhook-verify.js.map +1 -1
- package/dist/webhooks.d.ts +13 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +26 -0
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -9
- package/skills/sendora/SKILL.md +19 -9
- package/src/account.ts +40 -0
- package/src/client.ts +20 -43
- package/src/domains.ts +1 -1
- package/src/error.ts +36 -3
- package/src/inbound-domains.ts +89 -0
- package/src/inbound.ts +135 -0
- package/src/index.ts +8 -1
- package/src/options.ts +51 -0
- package/src/servers.ts +166 -0
- package/src/transport.ts +26 -5
- package/src/types.ts +194 -5
- package/src/version.ts +1 -1
- package/src/webhook-verify.ts +39 -18
- package/src/webhooks.ts +29 -0
package/src/inbound.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { paginate } from './pagination.ts';
|
|
2
|
+
import type { Transport } from './transport.ts';
|
|
3
|
+
import type {
|
|
4
|
+
InboundMessage,
|
|
5
|
+
InboundMessageDetail,
|
|
6
|
+
InboundPage,
|
|
7
|
+
InboundSearch,
|
|
8
|
+
InboundSearchBody,
|
|
9
|
+
RequestOptions,
|
|
10
|
+
} from './types.ts';
|
|
11
|
+
|
|
12
|
+
/** The mail the token's server has received on its inbound stream. */
|
|
13
|
+
export class InboundResource {
|
|
14
|
+
readonly #transport: Transport;
|
|
15
|
+
|
|
16
|
+
constructor(transport: Transport) {
|
|
17
|
+
this.#transport = transport;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One received message: its envelope, parties, headers, text and HTML,
|
|
22
|
+
* and its attachments described. The attachment bytes and the raw
|
|
23
|
+
* message are downloads of their own. Once the stream's content window
|
|
24
|
+
* has passed, `contentAvailable` is false and the content fields are
|
|
25
|
+
* null.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const message = await sendora.inbound.get(inboundMessageId);
|
|
29
|
+
* console.log(message.from?.address, message.subject, message.text);
|
|
30
|
+
*/
|
|
31
|
+
get(inboundMessageId: string, options: RequestOptions = {}): Promise<InboundMessageDetail> {
|
|
32
|
+
return this.#transport.request<InboundMessageDetail>({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
path: `/v1/inbound/${encodeURIComponent(inboundMessageId)}`,
|
|
35
|
+
idempotent: true,
|
|
36
|
+
signal: options.signal,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One page of received messages, newest first. Every filter is
|
|
42
|
+
* optional; pass the page's `next` as `after` for the following page,
|
|
43
|
+
* or use `searchAll`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const page = await sendora.inbound.search({ from: 'anna@example.com', limit: 20 });
|
|
47
|
+
*/
|
|
48
|
+
search(search: InboundSearch = {}, options: RequestOptions = {}): Promise<InboundPage> {
|
|
49
|
+
return this.#transport.request<InboundPage>({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/v1/inbound/search',
|
|
52
|
+
body: encodeInboundSearch(search),
|
|
53
|
+
idempotent: true,
|
|
54
|
+
signal: options.signal,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every received message the filters match, page by page, for `for await`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* for await (const message of sendora.inbound.searchAll({ recipient: address })) {
|
|
63
|
+
* console.log(message.receivedAt, message.subject);
|
|
64
|
+
* }
|
|
65
|
+
*/
|
|
66
|
+
searchAll(
|
|
67
|
+
search: InboundSearch = {},
|
|
68
|
+
options: RequestOptions = {},
|
|
69
|
+
): AsyncIterable<InboundMessage> {
|
|
70
|
+
return paginate(
|
|
71
|
+
(after) => this.search({ ...search, after }, options),
|
|
72
|
+
(page) => page.messages,
|
|
73
|
+
(page) => page.next,
|
|
74
|
+
search.after,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The message byte for byte as it was received, as `message/rfc822`.
|
|
80
|
+
* Throws `content_expired` once the stream's content window has passed.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const raw = await sendora.inbound.raw(inboundMessageId);
|
|
84
|
+
* await writeFile(`${inboundMessageId}.eml`, raw);
|
|
85
|
+
*/
|
|
86
|
+
raw(inboundMessageId: string, options: RequestOptions = {}): Promise<Uint8Array> {
|
|
87
|
+
return this.#transport.requestBytes({
|
|
88
|
+
method: 'GET',
|
|
89
|
+
path: `/v1/inbound/${encodeURIComponent(inboundMessageId)}/raw`,
|
|
90
|
+
idempotent: true,
|
|
91
|
+
signal: options.signal,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* An attachment's bytes, whatever the sender declared them to be; the
|
|
97
|
+
* name and the declared type are on the message. Throws
|
|
98
|
+
* `content_expired` once the stream's content window has passed.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const message = await sendora.inbound.get(inboundMessageId);
|
|
102
|
+
* for (const attachment of message.attachments) {
|
|
103
|
+
* const bytes = await sendora.inbound.attachment(inboundMessageId, attachment.attachmentId);
|
|
104
|
+
* }
|
|
105
|
+
*/
|
|
106
|
+
attachment(
|
|
107
|
+
inboundMessageId: string,
|
|
108
|
+
attachmentId: string,
|
|
109
|
+
options: RequestOptions = {},
|
|
110
|
+
): Promise<Uint8Array> {
|
|
111
|
+
return this.#transport.requestBytes({
|
|
112
|
+
method: 'GET',
|
|
113
|
+
path: `/v1/inbound/${encodeURIComponent(inboundMessageId)}/attachments/${encodeURIComponent(attachmentId)}`,
|
|
114
|
+
idempotent: true,
|
|
115
|
+
signal: options.signal,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The body as the API takes it: times as ISO 8601 strings. */
|
|
121
|
+
export function encodeInboundSearch(search: InboundSearch): InboundSearchBody {
|
|
122
|
+
const { receivedFrom, receivedTo, ...fields } = search;
|
|
123
|
+
const body: InboundSearchBody = { ...fields };
|
|
124
|
+
if (receivedFrom !== undefined) {
|
|
125
|
+
body.receivedFrom = isoOf(receivedFrom);
|
|
126
|
+
}
|
|
127
|
+
if (receivedTo !== undefined) {
|
|
128
|
+
body.receivedTo = isoOf(receivedTo);
|
|
129
|
+
}
|
|
130
|
+
return body;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isoOf(value: Date | string): string {
|
|
134
|
+
return typeof value === 'string' ? value : value.toISOString();
|
|
135
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
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';
|
|
5
7
|
export type { EmailResource } from './email.ts';
|
|
6
8
|
export { SendoraError } from './error.ts';
|
|
7
9
|
export type { SendoraErrorCode } from './error.ts';
|
|
10
|
+
export type { InboundResource } from './inbound.ts';
|
|
11
|
+
export type { InboundDomainsResource } from './inbound-domains.ts';
|
|
8
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';
|
|
9
16
|
export type { StreamsResource } from './streams.ts';
|
|
10
17
|
export type { SuppressionsResource } from './suppressions.ts';
|
|
11
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/transport.ts
CHANGED
|
@@ -46,11 +46,20 @@ export class Transport {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async request<T>(spec: RequestSpec): Promise<T> {
|
|
49
|
+
return this.#send<T>(spec, 'json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A download: the answer's bytes as they came, with the same errors and retries as any other request. */
|
|
53
|
+
async requestBytes(spec: RequestSpec): Promise<Uint8Array> {
|
|
54
|
+
return this.#send<Uint8Array>(spec, 'bytes');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async #send<T>(spec: RequestSpec, shape: 'json' | 'bytes'): Promise<T> {
|
|
49
58
|
const url = this.#url(spec);
|
|
50
59
|
const body = spec.body === undefined ? undefined : JSON.stringify(spec.body);
|
|
51
60
|
let waitedMs = 0;
|
|
52
61
|
for (let attempt = 0; ; attempt += 1) {
|
|
53
|
-
const outcome = await this.#attempt<T>(spec, url, body);
|
|
62
|
+
const outcome = await this.#attempt<T>(spec, url, body, shape);
|
|
54
63
|
if (outcome.ok) {
|
|
55
64
|
return outcome.value;
|
|
56
65
|
}
|
|
@@ -69,12 +78,17 @@ export class Transport {
|
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
async #attempt<T>(
|
|
81
|
+
async #attempt<T>(
|
|
82
|
+
spec: RequestSpec,
|
|
83
|
+
url: string,
|
|
84
|
+
body: string | undefined,
|
|
85
|
+
shape: 'json' | 'bytes',
|
|
86
|
+
): Promise<Attempt<T>> {
|
|
73
87
|
const timeout = AbortSignal.timeout(this.#options.timeoutMs);
|
|
74
88
|
const signal = spec.signal === undefined ? timeout : AbortSignal.any([spec.signal, timeout]);
|
|
75
89
|
const init: RequestInit = {
|
|
76
90
|
method: spec.method,
|
|
77
|
-
headers: this.#headers(spec, body),
|
|
91
|
+
headers: this.#headers(spec, body, shape),
|
|
78
92
|
signal,
|
|
79
93
|
redirect: 'manual',
|
|
80
94
|
};
|
|
@@ -113,6 +127,9 @@ export class Transport {
|
|
|
113
127
|
await response.body?.cancel();
|
|
114
128
|
return { ok: true, value: undefined as T };
|
|
115
129
|
}
|
|
130
|
+
if (shape === 'bytes' && response.ok) {
|
|
131
|
+
return { ok: true, value: new Uint8Array(await response.arrayBuffer()) as T };
|
|
132
|
+
}
|
|
116
133
|
const json = parseJson(await response.text());
|
|
117
134
|
if (response.status === 0 || (response.status >= 300 && response.status < 400)) {
|
|
118
135
|
return {
|
|
@@ -143,10 +160,14 @@ export class Transport {
|
|
|
143
160
|
};
|
|
144
161
|
}
|
|
145
162
|
|
|
146
|
-
#headers(
|
|
163
|
+
#headers(
|
|
164
|
+
spec: RequestSpec,
|
|
165
|
+
body: string | undefined,
|
|
166
|
+
shape: 'json' | 'bytes',
|
|
167
|
+
): Record<string, string> {
|
|
147
168
|
const headers: Record<string, string> = {
|
|
148
169
|
authorization: `Bearer ${this.#options.token}`,
|
|
149
|
-
accept: 'application/json',
|
|
170
|
+
accept: shape === 'bytes' ? '*/*' : 'application/json',
|
|
150
171
|
'user-agent': this.#options.userAgent,
|
|
151
172
|
...spec.headers,
|
|
152
173
|
};
|