@automate.ax/integration-contracts 0.98.0 → 0.99.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.
@@ -0,0 +1,430 @@
1
+ import * as z from "zod"
2
+
3
+ export const RESEND_EMAIL_STATUS_SCHEMA = z.enum([
4
+ "bounced",
5
+ "canceled",
6
+ "clicked",
7
+ "complained",
8
+ "delivered",
9
+ "delivery_delayed",
10
+ "failed",
11
+ "opened",
12
+ "queued",
13
+ "scheduled",
14
+ "sent",
15
+ "suppressed",
16
+ ])
17
+
18
+ export const RESEND_TAG_SCHEMA = z.object({
19
+ name: z.string(),
20
+ value: z.string(),
21
+ })
22
+
23
+ export const RESEND_ID_RESPONSE_SCHEMA = z.object({ id: z.string().min(1) })
24
+
25
+ export const RESEND_MUTATION_RESPONSE_SCHEMA = z.object({
26
+ id: z.string().min(1),
27
+ object: z.string(),
28
+ })
29
+
30
+ export const RESEND_EMAIL_MUTATION_RESPONSE_SCHEMA = z.object({
31
+ id: z.string().min(1),
32
+ object: z.literal("email"),
33
+ })
34
+
35
+ export const RESEND_CONTACT_MUTATION_RESPONSE_SCHEMA = z.object({
36
+ id: z.string().min(1),
37
+ object: z.literal("contact"),
38
+ })
39
+
40
+ export const RESEND_CONTACT_IMPORT_MUTATION_RESPONSE_SCHEMA = z.object({
41
+ id: z.string().min(1),
42
+ object: z.literal("contact_import"),
43
+ })
44
+
45
+ export const RESEND_EVENT_MUTATION_RESPONSE_SCHEMA = z.object({
46
+ id: z.string().min(1),
47
+ object: z.literal("event"),
48
+ })
49
+
50
+ export const RESEND_CONTACT_DELETE_RESPONSE_SCHEMA =
51
+ RESEND_CONTACT_MUTATION_RESPONSE_SCHEMA.extend({ deleted: z.boolean() })
52
+
53
+ export const RESEND_EVENT_DELETE_RESPONSE_SCHEMA =
54
+ RESEND_EVENT_MUTATION_RESPONSE_SCHEMA.extend({ deleted: z.boolean() })
55
+
56
+ export const RESEND_DELETE_RESPONSE_SCHEMA =
57
+ RESEND_MUTATION_RESPONSE_SCHEMA.extend({ deleted: z.boolean() })
58
+
59
+ export const RESEND_EMAIL_SUMMARY_WIRE_SCHEMA = z.object({
60
+ bcc: z.string().array().nullable().optional(),
61
+ cc: z.string().array().nullable().optional(),
62
+ created_at: z.string(),
63
+ from: z.string(),
64
+ id: z.string(),
65
+ last_event: RESEND_EMAIL_STATUS_SCHEMA,
66
+ message_id: z.string().nullable().optional(),
67
+ reply_to: z.string().array().nullable().optional(),
68
+ scheduled_at: z.string().nullable().optional(),
69
+ subject: z.string(),
70
+ to: z.string().array(),
71
+ topic_id: z.string().nullable().optional(),
72
+ })
73
+
74
+ export const RESEND_EMAIL_SUMMARY_SCHEMA =
75
+ RESEND_EMAIL_SUMMARY_WIRE_SCHEMA.transform(
76
+ ({
77
+ created_at: createdAt,
78
+ last_event: lastEvent,
79
+ message_id: messageId,
80
+ reply_to: replyTo,
81
+ scheduled_at: scheduledAt,
82
+ topic_id: topicId,
83
+ ...email
84
+ }) => ({
85
+ ...email,
86
+ createdAt,
87
+ lastEvent,
88
+ messageId,
89
+ replyTo,
90
+ scheduledAt,
91
+ topicId,
92
+ }),
93
+ )
94
+
95
+ export const RESEND_EMAIL_WIRE_SCHEMA = RESEND_EMAIL_SUMMARY_WIRE_SCHEMA.extend(
96
+ {
97
+ bcc: z.string().array().nullable().optional(),
98
+ cc: z.string().array().nullable().optional(),
99
+ html: z.string().nullable().optional(),
100
+ object: z.literal("email"),
101
+ reply_to: z.string().array().nullable().optional(),
102
+ tags: RESEND_TAG_SCHEMA.array().optional(),
103
+ text: z.string().nullable().optional(),
104
+ },
105
+ )
106
+
107
+ export const RESEND_EMAIL_SCHEMA = RESEND_EMAIL_WIRE_SCHEMA.transform(
108
+ ({
109
+ created_at: createdAt,
110
+ last_event: lastEvent,
111
+ message_id: messageId,
112
+ reply_to: replyTo,
113
+ scheduled_at: scheduledAt,
114
+ topic_id: topicId,
115
+ ...email
116
+ }) => ({
117
+ ...email,
118
+ createdAt,
119
+ lastEvent,
120
+ messageId,
121
+ replyTo,
122
+ scheduledAt,
123
+ topicId,
124
+ }),
125
+ )
126
+
127
+ export const RESEND_ATTACHMENT_WIRE_SCHEMA = z.object({
128
+ content_disposition: z.enum(["attachment", "inline"]).nullable(),
129
+ content_id: z.string(),
130
+ content_type: z.string(),
131
+ download_url: z.url(),
132
+ expires_at: z.string(),
133
+ filename: z.string().nullable(),
134
+ id: z.string(),
135
+ size: z.number().int().nonnegative(),
136
+ })
137
+
138
+ export const RESEND_ATTACHMENT_SCHEMA = RESEND_ATTACHMENT_WIRE_SCHEMA.transform(
139
+ ({
140
+ content_disposition: contentDisposition,
141
+ content_id: contentId,
142
+ content_type: contentType,
143
+ download_url: downloadUrl,
144
+ expires_at: expiresAt,
145
+ ...attachment
146
+ }) => ({
147
+ ...attachment,
148
+ contentDisposition,
149
+ contentId,
150
+ contentType,
151
+ downloadUrl,
152
+ expiresAt,
153
+ }),
154
+ )
155
+
156
+ const RESEND_RECEIVED_ATTACHMENT_WIRE_SCHEMA = z.object({
157
+ content_disposition: z.string().nullable(),
158
+ content_id: z.string().nullable(),
159
+ content_type: z.string(),
160
+ filename: z.string().nullable(),
161
+ id: z.string(),
162
+ size: z.number().int().nonnegative(),
163
+ })
164
+
165
+ const RESEND_RECEIVED_ATTACHMENT_SCHEMA =
166
+ RESEND_RECEIVED_ATTACHMENT_WIRE_SCHEMA.transform(
167
+ ({
168
+ content_disposition: contentDisposition,
169
+ content_id: contentId,
170
+ content_type: contentType,
171
+ ...attachment
172
+ }) => ({ ...attachment, contentDisposition, contentId, contentType }),
173
+ )
174
+
175
+ export const RESEND_RECEIVED_EMAIL_SUMMARY_WIRE_SCHEMA = z.object({
176
+ attachments: RESEND_RECEIVED_ATTACHMENT_WIRE_SCHEMA.array(),
177
+ bcc: z.string().array().nullable(),
178
+ cc: z.string().array().nullable(),
179
+ created_at: z.string(),
180
+ from: z.string(),
181
+ id: z.string(),
182
+ message_id: z.string(),
183
+ reply_to: z.string().array().nullable(),
184
+ subject: z.string().nullable(),
185
+ to: z.string().array(),
186
+ })
187
+
188
+ export const RESEND_RECEIVED_EMAIL_SUMMARY_SCHEMA =
189
+ RESEND_RECEIVED_EMAIL_SUMMARY_WIRE_SCHEMA.transform(
190
+ ({
191
+ attachments,
192
+ created_at: createdAt,
193
+ message_id: messageId,
194
+ reply_to: replyTo,
195
+ ...email
196
+ }) => ({
197
+ ...email,
198
+ attachments: attachments.map((attachment) =>
199
+ RESEND_RECEIVED_ATTACHMENT_SCHEMA.parse(attachment),
200
+ ),
201
+ createdAt,
202
+ messageId,
203
+ replyTo,
204
+ }),
205
+ )
206
+
207
+ export const RESEND_RECEIVED_EMAIL_WIRE_SCHEMA =
208
+ RESEND_RECEIVED_EMAIL_SUMMARY_WIRE_SCHEMA.extend({
209
+ headers: z.record(z.string(), z.string()).nullable(),
210
+ html: z.string().nullable(),
211
+ object: z.literal("email"),
212
+ raw: z
213
+ .object({ download_url: z.url(), expires_at: z.string() })
214
+ .nullable()
215
+ .optional(),
216
+ received_for: z.string().array().optional(),
217
+ subject: z.string(),
218
+ text: z.string().nullable(),
219
+ })
220
+
221
+ export const RESEND_RECEIVED_EMAIL_SCHEMA =
222
+ RESEND_RECEIVED_EMAIL_WIRE_SCHEMA.transform(
223
+ ({
224
+ attachments,
225
+ created_at: createdAt,
226
+ message_id: messageId,
227
+ raw,
228
+ received_for: receivedFor,
229
+ reply_to: replyTo,
230
+ ...email
231
+ }) => ({
232
+ ...email,
233
+ attachments: attachments.map((attachment) =>
234
+ RESEND_RECEIVED_ATTACHMENT_SCHEMA.parse(attachment),
235
+ ),
236
+ createdAt,
237
+ messageId,
238
+ raw:
239
+ raw === null || raw === undefined
240
+ ? raw
241
+ : { downloadUrl: raw.download_url, expiresAt: raw.expires_at },
242
+ receivedFor,
243
+ replyTo,
244
+ }),
245
+ )
246
+
247
+ export const RESEND_CONTACT_SUMMARY_WIRE_SCHEMA = z.object({
248
+ created_at: z.string(),
249
+ email: z.email(),
250
+ first_name: z.string().nullable(),
251
+ id: z.string(),
252
+ last_name: z.string().nullable(),
253
+ unsubscribed: z.boolean(),
254
+ })
255
+
256
+ export const RESEND_CONTACT_SUMMARY_SCHEMA =
257
+ RESEND_CONTACT_SUMMARY_WIRE_SCHEMA.transform(
258
+ ({
259
+ created_at: createdAt,
260
+ first_name: firstName,
261
+ last_name: lastName,
262
+ ...contact
263
+ }) => ({ ...contact, createdAt, firstName, lastName }),
264
+ )
265
+
266
+ export const RESEND_CONTACT_WIRE_SCHEMA =
267
+ RESEND_CONTACT_SUMMARY_WIRE_SCHEMA.extend({
268
+ object: z.literal("contact"),
269
+ properties: z.record(
270
+ z.string(),
271
+ z.discriminatedUnion("type", [
272
+ z.object({ type: z.literal("number"), value: z.number() }),
273
+ z.object({ type: z.literal("string"), value: z.string() }),
274
+ ]),
275
+ ),
276
+ })
277
+
278
+ export const RESEND_CONTACT_SCHEMA = RESEND_CONTACT_WIRE_SCHEMA.transform(
279
+ ({
280
+ created_at: createdAt,
281
+ first_name: firstName,
282
+ last_name: lastName,
283
+ ...contact
284
+ }) => ({ ...contact, createdAt, firstName, lastName }),
285
+ )
286
+
287
+ export const RESEND_SEGMENT_SUMMARY_WIRE_SCHEMA = z.object({
288
+ created_at: z.string(),
289
+ id: z.string(),
290
+ name: z.string(),
291
+ })
292
+
293
+ export const RESEND_SEGMENT_SUMMARY_SCHEMA =
294
+ RESEND_SEGMENT_SUMMARY_WIRE_SCHEMA.transform(
295
+ ({ created_at: createdAt, ...segment }) => ({ ...segment, createdAt }),
296
+ )
297
+
298
+ export const RESEND_CONTACT_IMPORT_COUNTS_SCHEMA = z.object({
299
+ created: z.number().int().nonnegative(),
300
+ failed: z.number().int().nonnegative(),
301
+ skipped: z.number().int().nonnegative(),
302
+ total: z.number().int().nonnegative(),
303
+ updated: z.number().int().nonnegative(),
304
+ })
305
+
306
+ export const RESEND_CONTACT_IMPORT_WIRE_SCHEMA = z.object({
307
+ completed_at: z.string().nullable().optional(),
308
+ counts: RESEND_CONTACT_IMPORT_COUNTS_SCHEMA.optional(),
309
+ created_at: z.string(),
310
+ id: z.string(),
311
+ object: z.literal("contact_import"),
312
+ status: z.enum(["completed", "failed", "in_progress", "queued"]),
313
+ })
314
+
315
+ export const RESEND_CONTACT_IMPORT_SCHEMA =
316
+ RESEND_CONTACT_IMPORT_WIRE_SCHEMA.transform(
317
+ ({
318
+ completed_at: completedAt,
319
+ created_at: createdAt,
320
+ ...contactImport
321
+ }) => ({ ...contactImport, completedAt, createdAt }),
322
+ )
323
+
324
+ export const RESEND_CONTACT_TOPIC_SCHEMA = z.object({
325
+ description: z.string(),
326
+ id: z.string(),
327
+ name: z.string(),
328
+ subscription: z.enum(["opt_in", "opt_out"]),
329
+ })
330
+
331
+ export const RESEND_CONTACT_SEGMENT_DELETE_WIRE_SCHEMA = z.object({
332
+ contact_id: z.string(),
333
+ deleted: z.boolean(),
334
+ object: z.literal("contact_segment"),
335
+ segment_id: z.string(),
336
+ })
337
+
338
+ export const RESEND_CONTACT_SEGMENT_MUTATION_WIRE_SCHEMA = z.object({
339
+ contact_id: z.string(),
340
+ object: z.literal("contact_segment"),
341
+ segment_id: z.string(),
342
+ })
343
+
344
+ export const RESEND_CONTACT_SEGMENT_MUTATION_SCHEMA =
345
+ RESEND_CONTACT_SEGMENT_MUTATION_WIRE_SCHEMA.transform(
346
+ ({ contact_id: contactId, segment_id: segmentId, ...result }) => ({
347
+ ...result,
348
+ contactId,
349
+ segmentId,
350
+ }),
351
+ )
352
+
353
+ export const RESEND_CONTACT_SEGMENT_DELETE_SCHEMA =
354
+ RESEND_CONTACT_SEGMENT_DELETE_WIRE_SCHEMA.transform(
355
+ ({ contact_id: contactId, segment_id: segmentId, ...result }) => ({
356
+ ...result,
357
+ contactId,
358
+ segmentId,
359
+ }),
360
+ )
361
+
362
+ export const RESEND_CONTACT_TOPICS_UPDATE_WIRE_SCHEMA = z.object({
363
+ contact_id: z.string(),
364
+ object: z.literal("contact_topics"),
365
+ topics: z
366
+ .object({
367
+ id: z.string(),
368
+ subscription: z.enum(["opt_in", "opt_out"]),
369
+ })
370
+ .array(),
371
+ })
372
+
373
+ export const RESEND_CONTACT_TOPICS_UPDATE_SCHEMA =
374
+ RESEND_CONTACT_TOPICS_UPDATE_WIRE_SCHEMA.transform(
375
+ ({ contact_id: contactId, ...result }) => ({ ...result, contactId }),
376
+ )
377
+
378
+ export const RESEND_EVENT_SEND_RESPONSE_SCHEMA = z.object({
379
+ event: z.string(),
380
+ object: z.literal("event"),
381
+ })
382
+
383
+ export const RESEND_EVENT_VALUE_TYPE_SCHEMA = z.enum([
384
+ "boolean",
385
+ "date",
386
+ "number",
387
+ "string",
388
+ ])
389
+
390
+ export const RESEND_EVENT_DEFINITION_WIRE_SCHEMA = z.object({
391
+ created_at: z.string(),
392
+ id: z.string(),
393
+ name: z.string(),
394
+ object: z.literal("event").optional(),
395
+ schema: z.record(z.string(), RESEND_EVENT_VALUE_TYPE_SCHEMA).nullable(),
396
+ updated_at: z.string().nullable(),
397
+ })
398
+
399
+ export const RESEND_EVENT_DEFINITION_SCHEMA =
400
+ RESEND_EVENT_DEFINITION_WIRE_SCHEMA.transform(
401
+ ({ created_at: createdAt, updated_at: updatedAt, ...event }) => ({
402
+ ...event,
403
+ createdAt,
404
+ updatedAt,
405
+ }),
406
+ )
407
+
408
+ /**
409
+ * Creates a Resend list response schema for one provider item schema.
410
+ *
411
+ * @param item - Provider item schema contained in the list.
412
+ */
413
+ export function resendListWireSchema<TItem extends z.ZodType>(item: TItem) {
414
+ return z.object({
415
+ data: item.array(),
416
+ has_more: z.boolean(),
417
+ object: z.literal("list"),
418
+ })
419
+ }
420
+
421
+ /**
422
+ * Creates a normalized Resend list response schema.
423
+ *
424
+ * @param item - Normalized item schema contained in the list.
425
+ */
426
+ export function resendListSchema<TItem extends z.ZodType>(item: TItem) {
427
+ return resendListWireSchema(item).transform(
428
+ ({ has_more: hasMore, ...page }) => ({ ...page, hasMore }),
429
+ )
430
+ }
@@ -0,0 +1,258 @@
1
+ import type { JsonValue } from "type-fest"
2
+ import * as z from "zod"
3
+
4
+ export * from "./action-schemas"
5
+
6
+ const RESEND_API_BASE_URL = "https://api.resend.com/"
7
+ const RESEND_API_ORIGIN = new URL(RESEND_API_BASE_URL).origin
8
+ export const RESEND_USER_AGENT = "automate.ax/resend"
9
+ const RESEND_API_KEY_SECRET_SCHEMA = z.object({
10
+ accessToken: z.never().optional(),
11
+ apiKey: z.string().min(1),
12
+ })
13
+ const RESEND_OAUTH_SECRET_SCHEMA = z.object({
14
+ accessToken: z.string().min(1),
15
+ apiKey: z.never().optional(),
16
+ })
17
+ const RESEND_SECRET_SCHEMA = z.union([
18
+ RESEND_API_KEY_SECRET_SCHEMA,
19
+ RESEND_OAUTH_SECRET_SCHEMA,
20
+ ])
21
+ const RESEND_ERROR_SCHEMA = z.looseObject({
22
+ message: z.string().optional(),
23
+ name: z.string().optional(),
24
+ statusCode: z.number().int().optional(),
25
+ })
26
+
27
+ /** Scalar value serialized into a Resend query parameter. */
28
+ type ResendQueryScalar = boolean | number | string
29
+
30
+ /** Provider rate-limit values returned with a Resend response. */
31
+ export interface ResendRateLimitMetadata {
32
+ /** Maximum requests allowed in the current window. */
33
+ limit?: number
34
+
35
+ /** Requests remaining in the current window. */
36
+ remaining?: number
37
+
38
+ /** Seconds until the current window resets. */
39
+ reset?: number
40
+ }
41
+
42
+ /** Provider email-quota usage returned with a Resend response. */
43
+ export interface ResendQuotaMetadata {
44
+ /** Used daily quota, when Resend supplies the free-plan header. */
45
+ daily?: number
46
+
47
+ /** Used monthly quota. */
48
+ monthly?: number
49
+ }
50
+
51
+ /** Options for one authenticated Resend API call. */
52
+ export interface ResendApiCallOptions<TSchema extends z.ZodType> {
53
+ /** Provider-native JSON or multipart request body. */
54
+ body?: FormData | JsonValue
55
+
56
+ /** Additional request headers such as `Idempotency-Key`. */
57
+ headers?: RequestInit["headers"]
58
+
59
+ /** HTTP method. Defaults to `POST` with a body and `GET` otherwise. */
60
+ httpMethod?: "DELETE" | "GET" | "PATCH" | "POST"
61
+
62
+ /** Provider-native URL query parameters. Array values are repeated. */
63
+ query?: Record<string, ResendQueryScalar | ResendQueryScalar[] | undefined>
64
+
65
+ /** Schema for the complete successful provider response. */
66
+ responseSchema: TSchema
67
+ }
68
+
69
+ /** Authenticated, schema-validating Resend REST API helper. */
70
+ export interface ResendApi {
71
+ /**
72
+ * Calls a Resend endpoint and validates its successful response.
73
+ *
74
+ * @param path - Resend API path such as `/emails`.
75
+ * @param options - Request data and response schema.
76
+ */
77
+ call<TSchema extends z.ZodType>(
78
+ path: string,
79
+ options: ResendApiCallOptions<TSchema>,
80
+ ): Promise<z.output<TSchema>>
81
+ }
82
+
83
+ /** Structured failure returned by the Resend API. */
84
+ export class ResendApiError extends Error {
85
+ /** API path that failed. */
86
+ readonly path: string
87
+
88
+ /** Provider error classification. */
89
+ readonly providerName?: string
90
+
91
+ /** Provider status code from the JSON error body. */
92
+ readonly providerStatusCode?: number
93
+
94
+ /** Email-quota usage returned with the failed response. */
95
+ readonly quota: ResendQuotaMetadata
96
+
97
+ /** Rate-limit values returned with the failed response. */
98
+ readonly rateLimit: ResendRateLimitMetadata
99
+
100
+ /** Delay in seconds before the request should be retried. */
101
+ readonly retryAfter?: number
102
+
103
+ /** HTTP status returned by Resend. */
104
+ readonly status: number
105
+
106
+ /**
107
+ * Creates a structured Resend provider error.
108
+ *
109
+ * @param options - Provider and transport failure details.
110
+ * @param options.message - Human-readable provider failure.
111
+ * @param options.path - API path that failed.
112
+ * @param options.providerName - Provider error classification.
113
+ * @param options.providerStatusCode - Provider status code from JSON.
114
+ * @param options.quota - Provider email-quota metadata.
115
+ * @param options.rateLimit - Provider rate-limit metadata.
116
+ * @param options.retryAfter - Provider retry delay in seconds.
117
+ * @param options.status - HTTP status.
118
+ */
119
+ constructor(options: {
120
+ message: string
121
+ path: string
122
+ providerName?: string
123
+ providerStatusCode?: number
124
+ quota: ResendQuotaMetadata
125
+ rateLimit: ResendRateLimitMetadata
126
+ retryAfter?: number
127
+ status: number
128
+ }) {
129
+ super(`Resend ${options.path} failed: ${options.message}`)
130
+ this.name = "ResendApiError"
131
+ this.path = options.path
132
+ this.providerName = options.providerName
133
+ this.providerStatusCode = options.providerStatusCode
134
+ this.quota = options.quota
135
+ this.rateLimit = options.rateLimit
136
+ this.retryAfter = options.retryAfter
137
+ this.status = options.status
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Creates a raw authenticated Resend REST client.
143
+ *
144
+ * The helper accepts either an API-key secret (`{ apiKey }`) or OAuth secret
145
+ * (`{ accessToken }`). Callers must supply a response schema for every
146
+ * request.
147
+ *
148
+ * @param secret - Resolved Resend integration secret.
149
+ */
150
+ export function getResendApi(secret: Record<string, unknown>): ResendApi {
151
+ const credential = RESEND_SECRET_SCHEMA.parse(secret)
152
+ const token =
153
+ "apiKey" in credential ? credential.apiKey : credential.accessToken
154
+
155
+ return {
156
+ call: async (path, options) => {
157
+ const url = new URL(path, RESEND_API_BASE_URL)
158
+ if (url.origin !== RESEND_API_ORIGIN) {
159
+ throw new Error("Resend API paths must use the Resend API origin.")
160
+ }
161
+ const normalizedPath = `${url.pathname}${url.search}`
162
+ for (const [name, value] of Object.entries(options.query ?? {})) {
163
+ for (const item of Array.isArray(value) ? value : [value]) {
164
+ if (item !== undefined) url.searchParams.append(name, String(item))
165
+ }
166
+ }
167
+
168
+ const headers = new Headers(options.headers)
169
+ headers.set("Accept", "application/json")
170
+ headers.set("Authorization", `Bearer ${token}`)
171
+ headers.set("User-Agent", RESEND_USER_AGENT)
172
+ const body = options.body
173
+ const isMultipart = body instanceof FormData
174
+ if (body !== undefined && !isMultipart) {
175
+ headers.set("Content-Type", "application/json")
176
+ }
177
+
178
+ const response = await fetch(url, {
179
+ body:
180
+ body instanceof FormData
181
+ ? body
182
+ : body === undefined
183
+ ? undefined
184
+ : JSON.stringify(body),
185
+ headers,
186
+ method:
187
+ options.httpMethod ?? (options.body === undefined ? "GET" : "POST"),
188
+ })
189
+ const responseText = await response.text()
190
+ const metadata = getResponseMetadata(response.headers)
191
+ if (!response.ok) {
192
+ const result = RESEND_ERROR_SCHEMA.safeParse(parseJson(responseText))
193
+ throw new ResendApiError({
194
+ message:
195
+ (result.success ? result.data.message : undefined) ??
196
+ (responseText.trim() || `HTTP ${response.status}`),
197
+ path: normalizedPath,
198
+ providerName: result.success ? result.data.name : undefined,
199
+ providerStatusCode: result.success
200
+ ? result.data.statusCode
201
+ : undefined,
202
+ status: response.status,
203
+ ...metadata,
204
+ })
205
+ }
206
+
207
+ return options.responseSchema.parse(
208
+ responseText.trim() ? parseJson(responseText) : undefined,
209
+ )
210
+ },
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Parses JSON while preserving useful plain-text error handling.
216
+ *
217
+ * @param value - Response text to parse.
218
+ */
219
+ function parseJson(value: string): unknown {
220
+ try {
221
+ return JSON.parse(value)
222
+ } catch {
223
+ return undefined
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Extracts retry, rate-limit, and quota headers from one response.
229
+ *
230
+ * @param headers - Resend response headers.
231
+ */
232
+ function getResponseMetadata(headers: Headers) {
233
+ return {
234
+ quota: {
235
+ daily: parseNumberHeader(headers, "x-resend-daily-quota"),
236
+ monthly: parseNumberHeader(headers, "x-resend-monthly-quota"),
237
+ },
238
+ rateLimit: {
239
+ limit: parseNumberHeader(headers, "ratelimit-limit"),
240
+ remaining: parseNumberHeader(headers, "ratelimit-remaining"),
241
+ reset: parseNumberHeader(headers, "ratelimit-reset"),
242
+ },
243
+ retryAfter: parseNumberHeader(headers, "retry-after"),
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Parses one numeric response header.
249
+ *
250
+ * @param headers - Resend response headers.
251
+ * @param name - Case-insensitive header name.
252
+ */
253
+ function parseNumberHeader(headers: Headers, name: string) {
254
+ const value = headers.get(name)
255
+ if (value === null) return undefined
256
+ const parsed = Number(value)
257
+ return Number.isFinite(parsed) ? parsed : undefined
258
+ }