ajo-kit-mail 0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2022-2026, Cristian Falcone
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14
+ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # ajo-kit-mail
2
+
3
+ Validated mail contract and pluggable transports for `ajo-kit` apps.
4
+
5
+ Includes:
6
+
7
+ - one validation boundary: every message is sealed before any transport sees it
8
+ - delivery with a hard deadline, bounded concurrency and a single attempt
9
+ - transports: SMTP (mandatory verified TLS), JSON HTTP providers, in-memory capture
10
+ - sanitized failures: classified codes and retry verdicts, never provider prose
11
+ - credential-free delivery events for observability
12
+ - integration with `ajo-kit`'s mail seam, so existing `send()` call sites gain
13
+ validation without being edited
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add ajo-kit-mail
19
+ ```
20
+
21
+ `ajo-kit-mail` requires `ajo-kit` as a peer dependency.
22
+
23
+ `nodemailer` is an optional peer used only by the SMTP transport. HTTP and
24
+ capture consumers do not install it:
25
+
26
+ ```bash
27
+ pnpm add nodemailer # only when using smtp()
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ Configure a transport once during app boot. `configure()` also adapts the
33
+ package into `ajo-kit`'s mail seam, so code importing `send` from
34
+ `ajo-kit/mail` delivers through it with validation and a deadline.
35
+
36
+ ```ts
37
+ import { configure } from 'ajo-kit-mail'
38
+ import { smtp } from 'ajo-kit-mail/smtp'
39
+
40
+ configure({
41
+ transport: smtp({ host: 'smtp.example.com', user: 'apikey', pass: process.env.SMTP_PASS }),
42
+ from: 'Ajo <noreply@example.com>',
43
+ })
44
+ ```
45
+
46
+ Development uses `capture()`; `configure()` refuses dev transports when
47
+ `NODE_ENV` is `production`.
48
+
49
+ ## Sending
50
+
51
+ ```ts
52
+ import { deliver, send } from 'ajo-kit-mail'
53
+
54
+ // Resolves to the message id, throws Refused or Undelivered.
55
+ await send({ to: 'user@example.com', subject: 'Reset', text: body })
56
+
57
+ // Never throws: a discriminated Outcome for code that branches on failure.
58
+ const outcome = await deliver({
59
+ to: { address: 'user@example.com', name: 'User' },
60
+ subject: 'Reset your password',
61
+ text: body,
62
+ kind: 'reset', // label for events: 'reset', 'verify', 'invite'
63
+ key: token.id, // idempotency key, forwarded to providers that accept one
64
+ expires: token.expires, // hard deadline: nothing is attempted past it
65
+ })
66
+
67
+ if (!outcome.ok && outcome.kind === 'undelivered' && outcome.retryable) retry()
68
+ ```
69
+
70
+ A message has exactly one recipient — credential mail must not fan out. The
71
+ sealed envelope carries an absolute deadline (default 10 s, capped by
72
+ `expires`) and an `AbortSignal` transports must honour.
73
+
74
+ `probe()` runs the transport's optional credential check without sending:
75
+
76
+ ```ts
77
+ const status = await probe() // { ok: true } | { ok: false, error }
78
+ ```
79
+
80
+ ## Transports
81
+
82
+ ### `smtp(options)`
83
+
84
+ One connection per message over nodemailer, with mandatory verified TLS:
85
+ STARTTLS is required on port 587 (`implicit: true` for 465), the certificate
86
+ is verified, and the floor is TLS 1.2. None of that is configurable, and
87
+ there is no credential URL form — discrete `host`/`user`/`pass` fields only.
88
+ Local development uses `capture()` instead of an insecure flag.
89
+
90
+ ### `http(options)`
91
+
92
+ A JSON provider over global `fetch`. The body mapping is the only
93
+ provider-specific code an app writes:
94
+
95
+ ```ts
96
+ import { http } from 'ajo-kit-mail/http'
97
+
98
+ const transport = http({
99
+ url: 'https://api.provider.example/send',
100
+ headers: () => ({ Authorization: `Bearer ${read()}` }), // factory: read per send
101
+ body: mail => ({ from: mail.from.address, to: mail.to.address, subject: mail.subject, text: mail.text }),
102
+ id: payload => (payload as { id?: string }).id,
103
+ })
104
+ ```
105
+
106
+ The message `key` is forwarded as `Idempotency-Key`. Success bodies are read
107
+ up to 64 KiB; failure bodies are cancelled unread.
108
+
109
+ ### `capture(options?)`
110
+
111
+ Bounded in-memory transport for tests and development — refused in
112
+ production:
113
+
114
+ ```ts
115
+ import { capture } from 'ajo-kit-mail/capture'
116
+
117
+ const mailbox = capture()
118
+ configure({ transport: mailbox, from: 'noreply@example.com' })
119
+
120
+ await send({ to: 'user@example.com', subject: 'Reset', text: `Open ${url}` })
121
+ mailbox.link(/\/reset\//) // first matching URL in the last body
122
+ mailbox.fail('throttled', 2) // drive the failure paths deterministically
123
+ ```
124
+
125
+ ### Custom transports
126
+
127
+ A transport is a function from a `Sealed` envelope to an optional `Receipt`.
128
+ It can only receive validated input — `seal()` is the sole constructor of
129
+ `Sealed`. Reuse `classify()` so failures inherit the sanitization guarantee:
130
+
131
+ ```ts
132
+ import { classify, type Transport } from 'ajo-kit-mail'
133
+
134
+ const transport: Transport = async mail => {
135
+ try {
136
+ await provider.send(/* ... */)
137
+ } catch (error) {
138
+ throw classify(error) // shape only: never message, cause or response bodies
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Outcomes and errors
144
+
145
+ `Refused` means the message or configuration was rejected before any network
146
+ work: `invalid-recipient`, `empty-body`, `too-large`, `expired`, and friends.
147
+ `Undelivered` means a transport accepted the envelope and the attempt failed,
148
+ with a classification (`timeout`, `auth`, `tls`, `throttled`, `rejected`, …),
149
+ a `retryable` verdict and at most a protocol status hint (`smtp 451`). Both
150
+ extend `ajo-kit`'s `Failure`; neither ever echoes an address, a subject, a
151
+ body or provider prose.
152
+
153
+ Validation limits: subject ≤ 255 bytes, body ≤ 256 KiB (hard maximum),
154
+ control characters rejected everywhere — the boundary that stops header
155
+ injection.
156
+
157
+ ## Observability
158
+
159
+ ```ts
160
+ configure({
161
+ transport,
162
+ from: 'noreply@example.com',
163
+ concurrency: 4, // backpressure, not throughput
164
+ observe: delivery => log(delivery), // { id, kind, transport, outcome, code?, retryable?, domain?, ms }
165
+ })
166
+ ```
167
+
168
+ Events are body-free by construction: the recipient appears as its domain
169
+ only, and no field can hold a credential. A throwing observer never changes
170
+ delivery semantics.
@@ -0,0 +1,82 @@
1
+ import { n as Undelivered } from "./chunks/errors-CMUvhZvz.js";
2
+ import { t as domain } from "./chunks/seal-BORkLlJV.js";
3
+ //#region packages/ajo-kit-mail/src/capture.ts
4
+ var DELIVERY = new Set([
5
+ "timeout",
6
+ "busy",
7
+ "connection",
8
+ "tls",
9
+ "auth",
10
+ "rejected",
11
+ "throttled",
12
+ "unavailable",
13
+ "unknown"
14
+ ]);
15
+ var RETRYABLE = new Set([
16
+ "timeout",
17
+ "busy",
18
+ "connection",
19
+ "throttled",
20
+ "unavailable"
21
+ ]);
22
+ var LINKS = /https?:\/\/[^\s<>"'`]+/g;
23
+ var count = (value) => {
24
+ const keep = value ?? 50;
25
+ if (!Number.isSafeInteger(keep) || keep < 0) throw new RangeError("Capture keep must be a non-negative safe integer");
26
+ return keep;
27
+ };
28
+ var repetitions = (value) => {
29
+ const times = value ?? 1;
30
+ if (!Number.isSafeInteger(times) || times <= 0) throw new RangeError("Capture failure count must be a positive safe integer");
31
+ return times;
32
+ };
33
+ var matches = (pattern, value) => {
34
+ const lastIndex = pattern.lastIndex;
35
+ pattern.lastIndex = 0;
36
+ try {
37
+ return pattern.test(value);
38
+ } finally {
39
+ pattern.lastIndex = lastIndex;
40
+ }
41
+ };
42
+ /** Creates a bounded in-memory transport for development and deterministic tests. */
43
+ function capture(options = {}) {
44
+ const keep = count(options.keep);
45
+ const messages = [];
46
+ let failure;
47
+ const transport = async (mail) => {
48
+ if (failure) {
49
+ const current = failure;
50
+ current.remaining--;
51
+ if (current.remaining === 0) failure = void 0;
52
+ throw new Undelivered(current.code, RETRYABLE.has(current.code));
53
+ }
54
+ messages.push(mail);
55
+ if (messages.length > keep) messages.splice(0, messages.length - keep);
56
+ if (options.log) console.log(`[mail] ${mail.id} ${mail.kind} @${domain(mail.to.address)} sent 0ms`);
57
+ return { id: mail.id };
58
+ };
59
+ return Object.assign(transport, {
60
+ label: "capture",
61
+ dev: true,
62
+ messages,
63
+ last: () => messages.at(-1),
64
+ link: (pattern) => {
65
+ const links = messages.at(-1)?.text.match(LINKS) ?? [];
66
+ return pattern ? links.find((value) => matches(pattern, value)) : links[0];
67
+ },
68
+ fail: (code, times) => {
69
+ if (!DELIVERY.has(code)) throw new RangeError("Unknown capture delivery code");
70
+ failure = {
71
+ code,
72
+ remaining: repetitions(times)
73
+ };
74
+ },
75
+ clear: () => {
76
+ messages.length = 0;
77
+ failure = void 0;
78
+ }
79
+ });
80
+ }
81
+ //#endregion
82
+ export { capture };
@@ -0,0 +1,97 @@
1
+ import { Failure } from "ajo-kit";
2
+ //#region packages/ajo-kit-mail/src/errors.ts
3
+ var CONFIGURATION = new Set([
4
+ "no-transport",
5
+ "invalid-config",
6
+ "invalid-sender"
7
+ ]);
8
+ var DELIVERY = new Set([
9
+ "timeout",
10
+ "busy",
11
+ "connection",
12
+ "tls",
13
+ "auth",
14
+ "rejected",
15
+ "throttled",
16
+ "unavailable",
17
+ "unknown"
18
+ ]);
19
+ var RETRYABLE = new Set([
20
+ "timeout",
21
+ "busy",
22
+ "connection",
23
+ "throttled",
24
+ "unavailable"
25
+ ]);
26
+ /**
27
+ * The message or the configuration was rejected. Nothing was sent and no provider
28
+ * was contacted. Extends Failure with a deliberate status so normalize() cannot
29
+ * inherit a provider status and server.tsx never console.error()s the object.
30
+ * The message is the code and never echoes an input value.
31
+ */
32
+ var Refused = class extends Failure {
33
+ code;
34
+ constructor(code) {
35
+ super(CONFIGURATION.has(code) ? 500 : 422, `Mail refused: ${code}`);
36
+ this.name = "Refused";
37
+ this.code = code;
38
+ if (CONFIGURATION.has(code)) console.error(`[mail] refused: ${code}`);
39
+ }
40
+ };
41
+ /**
42
+ * A transport accepted the envelope and the attempt failed. Carries a
43
+ * classification, a retry verdict and at most a protocol status. Never provider
44
+ * prose, never a recipient, never a subject, never a body, never a cause.
45
+ */
46
+ var Undelivered = class extends Failure {
47
+ code;
48
+ retryable;
49
+ /** Protocol status only: 'smtp 451', 'status 429'. */
50
+ hint;
51
+ constructor(code, retryable, hint) {
52
+ super(502, `Mail delivery failed: ${code}`);
53
+ this.name = "Undelivered";
54
+ this.code = code;
55
+ this.retryable = retryable;
56
+ this.hint = hint;
57
+ }
58
+ };
59
+ var protocol = (value) => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
60
+ /**
61
+ * Reduces any thrown value to an Undelivered by reading shape only: name, code,
62
+ * responseCode/status/statusCode. Never .message, never .cause, never .response,
63
+ * never .envelope. Exported so a third-party transport inherits the guarantee
64
+ * instead of reimplementing it.
65
+ */
66
+ function classify(error) {
67
+ const value = error !== null && (typeof error === "object" || typeof error === "function") ? error : {};
68
+ const rawName = value.name;
69
+ const rawCode = value.code;
70
+ const rawResponseCode = value.responseCode;
71
+ const rawStatus = value.status;
72
+ const rawStatusCode = value.statusCode;
73
+ const name = typeof rawName === "string" ? rawName : "";
74
+ const code = typeof rawCode === "string" ? rawCode : "";
75
+ const responseCode = protocol(rawResponseCode);
76
+ const directStatus = protocol(rawStatus);
77
+ const statusCode = protocol(rawStatusCode);
78
+ const status = responseCode ?? directStatus ?? statusCode;
79
+ const hint = status === void 0 ? void 0 : `${responseCode === void 0 ? "status" : "smtp"} ${status}`;
80
+ const undelivered = (delivery, retryable, detail) => new Undelivered(delivery, retryable, detail);
81
+ if (name === "Undelivered" && DELIVERY.has(code)) {
82
+ const delivery = code;
83
+ return undelivered(delivery, RETRYABLE.has(delivery));
84
+ }
85
+ if (code === "EAUTH" || responseCode === 530 || responseCode === 535 || responseCode === void 0 && (status === 401 || status === 403)) return undelivered("auth", false, hint);
86
+ if (code === "ETLS" || code.startsWith("ERR_TLS") || code.includes("CERT")) return undelivered("tls", false);
87
+ if (name === "AbortError" || name === "TimeoutError" || code === "ETIMEDOUT" || code === "ECONNECTION" && status === void 0 || responseCode === void 0 && status === 408) return undelivered("timeout", true);
88
+ if (responseCode !== void 0 && responseCode >= 400 && responseCode < 500) return undelivered("throttled", true, hint);
89
+ if (responseCode !== void 0 && responseCode >= 500) return undelivered("rejected", false, hint);
90
+ if (status === 429) return undelivered("throttled", true, hint);
91
+ if (status !== void 0 && status >= 500) return undelivered("unavailable", true, hint);
92
+ if (status !== void 0 && status >= 400) return undelivered("rejected", false, hint);
93
+ if (code) return undelivered("connection", true);
94
+ return undelivered("unknown", false);
95
+ }
96
+ //#endregion
97
+ export { Undelivered as n, classify as r, Refused as t };
@@ -0,0 +1,117 @@
1
+ import { t as Refused } from "./errors-CMUvhZvz.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { Buffer } from "node:buffer";
4
+ //#region packages/ajo-kit-mail/src/seal.ts
5
+ var CONTROL = /[\u0000-\u001f\u007f]/;
6
+ var ADDRESS = /^(?=.{3,254}$)[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
7
+ var NAME = /^[^"<>,;:\\\u0000-\u001f\u007f]{1,128}$/;
8
+ var KEY = /^[A-Za-z0-9._:-]{1,128}$/;
9
+ var KIND = /^[a-z][a-z0-9-]{0,31}$/;
10
+ var ADDRESS_BYTES = 254;
11
+ var SUBJECT_BYTES = 255;
12
+ var NAME_BYTES = 128;
13
+ var KEY_BYTES = 128;
14
+ var KIND_BYTES = 32;
15
+ var BODY_BYTES = 262144;
16
+ var TIMEOUT = 1e4;
17
+ var WORD_BYTES = 45;
18
+ var bytes = (value) => Buffer.byteLength(value, "utf8");
19
+ var refusal = (code) => {
20
+ throw new Refused(code);
21
+ };
22
+ var mailbox = (recipient, code) => {
23
+ const address = typeof recipient === "string" ? recipient : recipient?.address;
24
+ const name = typeof recipient === "string" ? void 0 : recipient?.name;
25
+ if (typeof address !== "string") refusal(code);
26
+ if (CONTROL.test(address) || !ADDRESS.test(address) || bytes(address) > ADDRESS_BYTES) refusal(code);
27
+ if (name !== void 0) {
28
+ if (typeof name !== "string" || CONTROL.test(name) || !NAME.test(name) || bytes(name) > NAME_BYTES) refusal("invalid-name");
29
+ }
30
+ return Object.freeze({
31
+ address,
32
+ ...name !== void 0 && { name }
33
+ });
34
+ };
35
+ var duration = (value) => {
36
+ const timeout = value ?? TIMEOUT;
37
+ if (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > 2147483647) refusal("invalid-config");
38
+ return timeout;
39
+ };
40
+ var bodyLimit = (value) => {
41
+ const limit = value ?? BODY_BYTES;
42
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > BODY_BYTES) refusal("invalid-config");
43
+ return limit;
44
+ };
45
+ var expiry = (value) => {
46
+ if (value === void 0) return Infinity;
47
+ const time = value instanceof Date ? value.getTime() : value;
48
+ return typeof time === "number" && Number.isFinite(time) ? time : refusal("expired");
49
+ };
50
+ /** Validates once, on the way in. Every later stage consumes only the result. */
51
+ function seal(message, policy) {
52
+ if (!message || typeof message !== "object" || !policy || typeof policy !== "object") refusal("invalid-config");
53
+ const toRecipient = message.to;
54
+ const subject = message.subject;
55
+ const text = message.text;
56
+ const html = message.html;
57
+ const fromRecipient = message.from;
58
+ const messageReply = message.replyTo;
59
+ const rawKind = message.kind;
60
+ const key = message.key;
61
+ const expires = message.expires;
62
+ const policyFrom = policy.from;
63
+ const policyReply = policy.replyTo;
64
+ const policyTimeout = policy.timeout;
65
+ const policyLimit = policy.limit;
66
+ const deadline = Math.min(Date.now() + duration(policyTimeout), expiry(expires));
67
+ const limit = bodyLimit(policyLimit);
68
+ const from = mailbox(fromRecipient ?? policyFrom, "invalid-sender");
69
+ const to = mailbox(toRecipient, "invalid-recipient");
70
+ const replyRecipient = messageReply ?? policyReply;
71
+ const replyTo = replyRecipient === void 0 ? void 0 : mailbox(replyRecipient, "invalid-recipient");
72
+ const kind = rawKind ?? "mail";
73
+ if (typeof subject !== "string" || !subject || CONTROL.test(subject) || bytes(subject) > SUBJECT_BYTES) refusal("invalid-subject");
74
+ if (typeof kind !== "string" || CONTROL.test(kind) || !KIND.test(kind) || bytes(kind) > KIND_BYTES) refusal("invalid-kind");
75
+ if (key !== void 0 && (typeof key !== "string" || CONTROL.test(key) || !KEY.test(key) || bytes(key) > KEY_BYTES)) refusal("invalid-key");
76
+ if (typeof text !== "string" || html !== void 0 && typeof html !== "string" || !text && !html) refusal("empty-body");
77
+ if (bytes(text) + bytes(html ?? "") > limit) refusal("too-large");
78
+ if (deadline <= Date.now()) refusal("expired");
79
+ const signal = AbortSignal.timeout(Math.max(0, Math.ceil(deadline - Date.now())));
80
+ return Object.freeze({
81
+ id: randomUUID(),
82
+ kind,
83
+ from,
84
+ to,
85
+ ...replyTo && { replyTo },
86
+ subject,
87
+ text,
88
+ ...html !== void 0 && { html },
89
+ ...key !== void 0 && { key },
90
+ deadline,
91
+ signal
92
+ });
93
+ }
94
+ /** Returns the recipient domain, the only address part safe for logs. */
95
+ function domain(address) {
96
+ return address.slice(address.lastIndexOf("@") + 1).toLowerCase();
97
+ }
98
+ /** Encodes and folds a value into RFC 2047 encoded-words of at most 75 characters. */
99
+ function encode(value) {
100
+ const chunks = [];
101
+ let chunk = "";
102
+ let size = 0;
103
+ for (const character of value) {
104
+ const width = bytes(character);
105
+ if (size + width > WORD_BYTES && chunk) {
106
+ chunks.push(chunk);
107
+ chunk = "";
108
+ size = 0;
109
+ }
110
+ chunk += character;
111
+ size += width;
112
+ }
113
+ chunks.push(chunk);
114
+ return chunks.map((part) => `=?UTF-8?B?${Buffer.from(part, "utf8").toString("base64")}?=`).join("\r\n ");
115
+ }
116
+ //#endregion
117
+ export { encode as n, seal as r, domain as t };
package/dist/http.js ADDED
@@ -0,0 +1,65 @@
1
+ import { r as classify } from "./chunks/errors-CMUvhZvz.js";
2
+ //#region packages/ajo-kit-mail/src/http.ts
3
+ var RESPONSE_BYTES = 65536;
4
+ var payload = async (response) => {
5
+ if (!response.body) return;
6
+ const reader = response.body.getReader();
7
+ const bytes = new Uint8Array(RESPONSE_BYTES);
8
+ let size = 0;
9
+ let complete = false;
10
+ try {
11
+ while (size < RESPONSE_BYTES) {
12
+ const chunk = await reader.read();
13
+ if (chunk.done) {
14
+ complete = true;
15
+ break;
16
+ }
17
+ const length = Math.min(chunk.value.byteLength, RESPONSE_BYTES - size);
18
+ bytes.set(chunk.value.subarray(0, length), size);
19
+ size += length;
20
+ }
21
+ } finally {
22
+ if (complete) reader.releaseLock();
23
+ else await reader.cancel().catch(() => {});
24
+ }
25
+ if (!size) return;
26
+ return JSON.parse(new TextDecoder().decode(bytes.subarray(0, size)));
27
+ };
28
+ /**
29
+ * Creates a provider transport over global fetch. Non-success bodies are
30
+ * cancelled unread, while success bodies are retained only up to 64 KiB.
31
+ */
32
+ function http(options) {
33
+ const transport = async (mail) => {
34
+ let response;
35
+ try {
36
+ const configured = typeof options.headers === "function" ? options.headers() : options.headers;
37
+ const headers = new Headers(configured);
38
+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
39
+ headers.delete("Idempotency-Key");
40
+ if (mail.key !== void 0) headers.set("Idempotency-Key", mail.key);
41
+ response = await fetch(options.url, {
42
+ method: "POST",
43
+ headers,
44
+ body: JSON.stringify(options.body(mail)),
45
+ signal: mail.signal
46
+ });
47
+ } catch (error) {
48
+ throw classify(error);
49
+ }
50
+ if (!response.ok) {
51
+ await response.body?.cancel().catch(() => {});
52
+ throw classify({ status: response.status });
53
+ }
54
+ try {
55
+ const result = await payload(response);
56
+ const id = options.id?.(result);
57
+ return id === void 0 ? void 0 : { id };
58
+ } catch (error) {
59
+ throw classify(error);
60
+ }
61
+ };
62
+ return Object.assign(transport, { label: options.label ?? "http" });
63
+ }
64
+ //#endregion
65
+ export { http };