From ab1d5091484a7cc1ccad9ae5980124054fa88c57 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Tue, 30 Sep 2025 11:52:02 -0300 Subject: [PATCH 01/49] feat: get inbound emails v0 (#641) --- src/attachments/attachments.spec.ts | 298 ++++++++++++++++++ src/attachments/attachments.ts | 84 +++++ src/attachments/interfaces/attachment.ts | 8 + .../interfaces/get-attachment.interface.ts | 36 +++ src/attachments/interfaces/index.ts | 10 + .../interfaces/list-attachments.interface.ts | 29 ++ src/inbound/inbound.spec.ts | 165 ++++++++++ src/inbound/inbound.ts | 52 +++ .../interfaces/get-inbound-email.interface.ts | 39 +++ src/inbound/interfaces/inbound-email.ts | 21 ++ src/inbound/interfaces/index.ts | 5 + src/index.ts | 2 + src/resend.ts | 4 + 13 files changed, 753 insertions(+) create mode 100644 src/attachments/attachments.spec.ts create mode 100644 src/attachments/attachments.ts create mode 100644 src/attachments/interfaces/attachment.ts create mode 100644 src/attachments/interfaces/get-attachment.interface.ts create mode 100644 src/attachments/interfaces/index.ts create mode 100644 src/attachments/interfaces/list-attachments.interface.ts create mode 100644 src/inbound/inbound.spec.ts create mode 100644 src/inbound/inbound.ts create mode 100644 src/inbound/interfaces/get-inbound-email.interface.ts create mode 100644 src/inbound/interfaces/inbound-email.ts create mode 100644 src/inbound/interfaces/index.ts diff --git a/src/attachments/attachments.spec.ts b/src/attachments/attachments.spec.ts new file mode 100644 index 00000000..50f1beec --- /dev/null +++ b/src/attachments/attachments.spec.ts @@ -0,0 +1,298 @@ +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Attachments', () => { + afterEach(() => fetchMock.resetMocks()); + + describe('get', () => { + describe('when attachment not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Attachment not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.get({ + inboundId: '61cda979-919d-4b9d-9638-c148b93ff410', + id: 'att_123', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Attachment not found", + "name": "not_found", + }, +} +`); + }); + }); + + describe('when attachment found', () => { + it('returns attachment with transformed fields', async () => { + const apiResponse = { + object: 'attachment' as const, + data: { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + content: 'base64encodedcontent==', + }, + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.get({ + inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_123', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "data": { + "content": "base64encodedcontent==", + "contentDisposition": "attachment", + "contentId": "cid_123", + "contentType": "application/pdf", + "filename": "document.pdf", + "id": "att_123", + }, + "object": "attachment", + }, + "error": null, +} +`); + }); + + it('returns inline attachment', async () => { + const apiResponse = { + object: 'attachment' as const, + data: { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + content: + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', + }, + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.get({ + inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_456', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "data": { + "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "contentDisposition": "inline", + "contentId": "cid_456", + "contentType": "image/png", + "filename": "image.png", + "id": "att_456", + }, + "object": "attachment", + }, + "error": null, +} +`); + }); + + it('handles attachment without optional fields (filename, contentId)', async () => { + const apiResponse = { + object: 'attachment' as const, + data: { + // Required fields based on DB schema + id: 'att_789', + content_type: 'text/plain', + content_disposition: 'attachment' as const, + content: 'base64content', + // Optional fields (filename, content_id) omitted + }, + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.get({ + inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_789', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "data": { + "content": "base64content", + "contentDisposition": "attachment", + "contentId": undefined, + "contentType": "text/plain", + "filename": undefined, + "id": "att_789", + }, + "object": "attachment", + }, + "error": null, +} +`); + }); + }); + }); + + describe('list', () => { + describe('when inbound email not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Inbound email not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.list({ + inboundId: '61cda979-919d-4b9d-9638-c148b93ff410', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Inbound email not found", + "name": "not_found", + }, +} +`); + }); + }); + + describe('when attachments found', () => { + it('returns multiple attachments with transformed fields', async () => { + const apiResponse = { + object: 'attachment' as const, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + content: 'base64encodedcontent==', + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + content: 'imagebase64==', + }, + ], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.list({ + inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": [ + { + "content": "base64encodedcontent==", + "contentDisposition": "attachment", + "contentId": "cid_123", + "contentType": "application/pdf", + "filename": "document.pdf", + "id": "att_123", + }, + { + "content": "imagebase64==", + "contentDisposition": "inline", + "contentId": "cid_456", + "contentType": "image/png", + "filename": "image.png", + "id": "att_456", + }, + ], + "error": null, +} +`); + }); + + it('returns empty array when no attachments', async () => { + const apiResponse = { + object: 'attachment' as const, + data: [], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.list({ + inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": [], + "error": null, +} +`); + }); + }); + }); +}); diff --git a/src/attachments/attachments.ts b/src/attachments/attachments.ts new file mode 100644 index 00000000..7ac622f0 --- /dev/null +++ b/src/attachments/attachments.ts @@ -0,0 +1,84 @@ +import type { Resend } from '../resend'; +import type { + GetAttachmentApiResponse, + GetAttachmentOptions, + GetAttachmentResponse, + GetAttachmentResponseSuccess, +} from './interfaces/get-attachment.interface'; +import type { + ListAttachmentsApiResponse, + ListAttachmentsOptions, + ListAttachmentsResponse, +} from './interfaces/list-attachments.interface'; + +export class Attachments { + constructor(private readonly resend: Resend) {} + + async get(options: GetAttachmentOptions): Promise { + const { inboundId, id } = options; + + const data = await this.resend.get( + `/emails/inbound/${inboundId}/attachments/${id}`, + ); + + if (data.error) { + return { + data: null, + error: data.error, + }; + } + + const apiResponse = data.data; + + const transformedData: GetAttachmentResponseSuccess = { + object: apiResponse.object, + data: { + id: apiResponse.data.id, + filename: apiResponse.data.filename, + contentType: apiResponse.data.content_type, + contentDisposition: apiResponse.data.content_disposition, + contentId: apiResponse.data.content_id, + content: apiResponse.data.content, + }, + }; + + return { + data: transformedData, + error: null, + }; + } + + async list( + options: ListAttachmentsOptions, + ): Promise { + const { inboundId } = options; + + const data = await this.resend.get( + `/emails/inbound/${inboundId}/attachments`, + ); + + if (data.error) { + return { + data: null, + error: data.error, + }; + } + + const apiResponse = data.data; + + // Transform snake_case to camelCase and return array directly + const transformedData = apiResponse.data.map((attachment) => ({ + id: attachment.id, + filename: attachment.filename, + contentType: attachment.content_type, + contentDisposition: attachment.content_disposition, + contentId: attachment.content_id, + content: attachment.content, + })); + + return { + data: transformedData, + error: null, + }; + } +} diff --git a/src/attachments/interfaces/attachment.ts b/src/attachments/interfaces/attachment.ts new file mode 100644 index 00000000..b59c1292 --- /dev/null +++ b/src/attachments/interfaces/attachment.ts @@ -0,0 +1,8 @@ +export interface InboundAttachment { + id: string; + filename?: string; + contentType: string; + contentDisposition: 'inline' | 'attachment'; + contentId?: string; + content: string; // base64 +} diff --git a/src/attachments/interfaces/get-attachment.interface.ts b/src/attachments/interfaces/get-attachment.interface.ts new file mode 100644 index 00000000..700c1c80 --- /dev/null +++ b/src/attachments/interfaces/get-attachment.interface.ts @@ -0,0 +1,36 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { InboundAttachment } from './attachment'; + +export interface GetAttachmentOptions { + inboundId: string; + id: string; +} + +// API response type (snake_case from API) +export interface GetAttachmentApiResponse { + object: 'attachment'; + data: { + id: string; + filename?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; + content: string; + }; +} + +// SDK response type (camelCase for users) +export interface GetAttachmentResponseSuccess { + object: 'attachment'; + data: InboundAttachment; +} + +export type GetAttachmentResponse = + | { + data: GetAttachmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/attachments/interfaces/index.ts b/src/attachments/interfaces/index.ts new file mode 100644 index 00000000..ec3f8400 --- /dev/null +++ b/src/attachments/interfaces/index.ts @@ -0,0 +1,10 @@ +export type { InboundAttachment } from './attachment'; +export type { + GetAttachmentOptions, + GetAttachmentResponse, + GetAttachmentResponseSuccess, +} from './get-attachment.interface'; +export type { + ListAttachmentsOptions, + ListAttachmentsResponse, +} from './list-attachments.interface'; diff --git a/src/attachments/interfaces/list-attachments.interface.ts b/src/attachments/interfaces/list-attachments.interface.ts new file mode 100644 index 00000000..966c7a78 --- /dev/null +++ b/src/attachments/interfaces/list-attachments.interface.ts @@ -0,0 +1,29 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { InboundAttachment } from './attachment'; + +export interface ListAttachmentsOptions { + inboundId: string; +} + +// API response type (snake_case from API) +export interface ListAttachmentsApiResponse { + object: 'attachment'; + data: Array<{ + id: string; + filename?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; + content: string; + }>; +} + +export type ListAttachmentsResponse = + | { + data: InboundAttachment[]; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/inbound/inbound.spec.ts b/src/inbound/inbound.spec.ts new file mode 100644 index 00000000..16366645 --- /dev/null +++ b/src/inbound/inbound.spec.ts @@ -0,0 +1,165 @@ +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Inbound', () => { + afterEach(() => fetchMock.resetMocks()); + + describe('get', () => { + describe('when inbound email not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Inbound email not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = resend.inbound.get( + '61cda979-919d-4b9d-9638-c148b93ff410', + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Inbound email not found", + "name": "not_found", + }, +} +`); + }); + }); + + describe('when inbound email found', () => { + it('returns inbound email with transformed fields', async () => { + const apiResponse = { + object: 'inbound' as const, + id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + to: ['received@example.com'], + from: 'sender@example.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + subject: 'Test inbound email', + html: '

hello world

', + text: 'hello world', + bcc: null, + cc: ['cc@example.com'], + reply_to: ['reply@example.com'], + attachments: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + }, + ], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.inbound.get( + '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + ); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "attachments": [ + { + "contentDisposition": "attachment", + "contentId": "cid_123", + "contentType": "application/pdf", + "filename": "document.pdf", + "id": "att_123", + }, + ], + "bcc": null, + "cc": [ + "cc@example.com", + ], + "createdAt": "2023-04-07T23:13:52.669661+00:00", + "from": "sender@example.com", + "html": "

hello world

", + "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + "object": "inbound", + "replyTo": [ + "reply@example.com", + ], + "subject": "Test inbound email", + "text": "hello world", + "to": [ + "received@example.com", + ], + }, + "error": null, +} +`); + }); + + it('returns inbound email with no attachments', async () => { + const apiResponse = { + object: 'inbound' as const, + id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + to: ['received@example.com'], + from: 'sender@example.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + subject: 'Test inbound email', + html: null, + text: 'hello world', + bcc: null, + cc: null, + reply_to: null, + attachments: [], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.inbound.get( + '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + ); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "attachments": [], + "bcc": null, + "cc": null, + "createdAt": "2023-04-07T23:13:52.669661+00:00", + "from": "sender@example.com", + "html": null, + "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + "object": "inbound", + "replyTo": null, + "subject": "Test inbound email", + "text": "hello world", + "to": [ + "received@example.com", + ], + }, + "error": null, +} +`); + }); + }); + }); +}); diff --git a/src/inbound/inbound.ts b/src/inbound/inbound.ts new file mode 100644 index 00000000..df24cce1 --- /dev/null +++ b/src/inbound/inbound.ts @@ -0,0 +1,52 @@ +import type { Resend } from '../resend'; +import type { + GetInboundEmailApiResponse, + GetInboundEmailResponse, + GetInboundEmailResponseSuccess, +} from './interfaces/get-inbound-email.interface'; + +export class Inbound { + constructor(private readonly resend: Resend) {} + + async get(id: string): Promise { + const data = await this.resend.get( + `/emails/inbound/${id}`, + ); + + if (data.error) { + return { + data: null, + error: data.error, + }; + } + + const apiResponse = data.data; + + // Transform snake_case to camelCase + const transformedData: GetInboundEmailResponseSuccess = { + object: apiResponse.object, + id: apiResponse.id, + to: apiResponse.to, + from: apiResponse.from, + createdAt: apiResponse.created_at, + subject: apiResponse.subject, + bcc: apiResponse.bcc, + cc: apiResponse.cc, + replyTo: apiResponse.reply_to, + html: apiResponse.html, + text: apiResponse.text, + attachments: apiResponse.attachments.map((attachment) => ({ + id: attachment.id, + filename: attachment.filename, + contentType: attachment.content_type, + contentId: attachment.content_id, + contentDisposition: attachment.content_disposition, + })), + }; + + return { + data: transformedData, + error: null, + }; + } +} diff --git a/src/inbound/interfaces/get-inbound-email.interface.ts b/src/inbound/interfaces/get-inbound-email.interface.ts new file mode 100644 index 00000000..aef3ba2a --- /dev/null +++ b/src/inbound/interfaces/get-inbound-email.interface.ts @@ -0,0 +1,39 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { InboundEmail } from './inbound-email'; + +// API response type (snake_case from API) +export interface GetInboundEmailApiResponse { + object: 'inbound'; + id: string; + to: string[]; + from: string; + created_at: string; + subject: string; + bcc: string[] | null; + cc: string[] | null; + reply_to: string[] | null; + html: string | null; + text: string | null; + attachments: Array<{ + id: string; + filename: string; + content_type: string; + content_id: string; + content_disposition: string; + }>; +} + +// SDK response type (camelCase for users) +export interface GetInboundEmailResponseSuccess extends InboundEmail { + object: 'inbound'; +} + +export type GetInboundEmailResponse = + | { + data: GetInboundEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/inbound/interfaces/inbound-email.ts b/src/inbound/interfaces/inbound-email.ts new file mode 100644 index 00000000..b00a4c13 --- /dev/null +++ b/src/inbound/interfaces/inbound-email.ts @@ -0,0 +1,21 @@ +export interface InboundEmail { + id: string; + to: string[]; + from: string; + createdAt: string; + subject: string; + bcc: string[] | null; + cc: string[] | null; + replyTo: string[] | null; + html: string | null; + text: string | null; + attachments: InboundEmailAttachment[]; +} + +export interface InboundEmailAttachment { + id: string; + filename: string; + contentType: string; + contentId: string; + contentDisposition: string; +} diff --git a/src/inbound/interfaces/index.ts b/src/inbound/interfaces/index.ts new file mode 100644 index 00000000..75aeb194 --- /dev/null +++ b/src/inbound/interfaces/index.ts @@ -0,0 +1,5 @@ +export type { + GetInboundEmailResponse, + GetInboundEmailResponseSuccess, +} from './get-inbound-email.interface'; +export type { InboundEmail, InboundEmailAttachment } from './inbound-email'; diff --git a/src/index.ts b/src/index.ts index e865109e..64646851 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './api-keys/interfaces'; +export * from './attachments/interfaces'; export * from './audiences/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; @@ -6,5 +7,6 @@ export * from './common/interfaces'; export * from './contacts/interfaces'; export * from './domains/interfaces'; export * from './emails/interfaces'; +export * from './inbound/interfaces'; export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; diff --git a/src/resend.ts b/src/resend.ts index 217535e6..e4a08e61 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -1,5 +1,6 @@ import { version } from '../package.json'; import { ApiKeys } from './api-keys/api-keys'; +import { Attachments } from './attachments/attachments'; import { Audiences } from './audiences/audiences'; import { Batch } from './batch/batch'; import { Broadcasts } from './broadcasts/broadcasts'; @@ -9,6 +10,7 @@ import type { PatchOptions } from './common/interfaces/patch-option.interface'; import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; +import { Inbound } from './inbound/inbound'; import type { ErrorResponse } from './interfaces'; import { Webhooks } from './webhooks/webhooks'; @@ -27,6 +29,7 @@ export class Resend { private readonly headers: Headers; readonly apiKeys = new ApiKeys(this); + readonly attachments = new Attachments(this); readonly audiences = new Audiences(this); readonly batch = new Batch(this); readonly broadcasts = new Broadcasts(this); @@ -34,6 +37,7 @@ export class Resend { readonly domains = new Domains(this); readonly emails = new Emails(this); readonly webhooks = new Webhooks(); + readonly inbound = new Inbound(this); constructor(readonly key?: string) { if (!key) { From ae5b029cfc1c61beb4f9ed3a80fa3f9f592b79c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:53:51 -0300 Subject: [PATCH 02/49] chore(deps): update dependency @biomejs/biome to v2.2.4 (#537) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 74 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 7f40d664..233f594c 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ } }, "devDependencies": { - "@biomejs/biome": "2.2.0", + "@biomejs/biome": "2.2.4", "@types/node": "22.18.6", "@types/react": "19.1.15", "pkg-pr-new": "0.0.60", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b26e5b7f..03529899 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: version: 1.76.1 devDependencies: '@biomejs/biome': - specifier: 2.2.0 - version: 2.2.0 + specifier: 2.2.4 + version: 2.2.4 '@types/node': specifier: 22.18.6 version: 22.18.6 @@ -54,55 +54,55 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} - '@biomejs/biome@2.2.0': - resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==} + '@biomejs/biome@2.2.4': + resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.0': - resolution: {integrity: sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==} + '@biomejs/cli-darwin-arm64@2.2.4': + resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.0': - resolution: {integrity: sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==} + '@biomejs/cli-darwin-x64@2.2.4': + resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.0': - resolution: {integrity: sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==} + '@biomejs/cli-linux-arm64-musl@2.2.4': + resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.0': - resolution: {integrity: sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==} + '@biomejs/cli-linux-arm64@2.2.4': + resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.0': - resolution: {integrity: sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==} + '@biomejs/cli-linux-x64-musl@2.2.4': + resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.0': - resolution: {integrity: sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==} + '@biomejs/cli-linux-x64@2.2.4': + resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.0': - resolution: {integrity: sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==} + '@biomejs/cli-win32-arm64@2.2.4': + resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.0': - resolution: {integrity: sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==} + '@biomejs/cli-win32-x64@2.2.4': + resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1537,39 +1537,39 @@ snapshots: '@actions/io@1.1.3': {} - '@biomejs/biome@2.2.0': + '@biomejs/biome@2.2.4': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.0 - '@biomejs/cli-darwin-x64': 2.2.0 - '@biomejs/cli-linux-arm64': 2.2.0 - '@biomejs/cli-linux-arm64-musl': 2.2.0 - '@biomejs/cli-linux-x64': 2.2.0 - '@biomejs/cli-linux-x64-musl': 2.2.0 - '@biomejs/cli-win32-arm64': 2.2.0 - '@biomejs/cli-win32-x64': 2.2.0 + '@biomejs/cli-darwin-arm64': 2.2.4 + '@biomejs/cli-darwin-x64': 2.2.4 + '@biomejs/cli-linux-arm64': 2.2.4 + '@biomejs/cli-linux-arm64-musl': 2.2.4 + '@biomejs/cli-linux-x64': 2.2.4 + '@biomejs/cli-linux-x64-musl': 2.2.4 + '@biomejs/cli-win32-arm64': 2.2.4 + '@biomejs/cli-win32-x64': 2.2.4 - '@biomejs/cli-darwin-arm64@2.2.0': + '@biomejs/cli-darwin-arm64@2.2.4': optional: true - '@biomejs/cli-darwin-x64@2.2.0': + '@biomejs/cli-darwin-x64@2.2.4': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.0': + '@biomejs/cli-linux-arm64-musl@2.2.4': optional: true - '@biomejs/cli-linux-arm64@2.2.0': + '@biomejs/cli-linux-arm64@2.2.4': optional: true - '@biomejs/cli-linux-x64-musl@2.2.0': + '@biomejs/cli-linux-x64-musl@2.2.4': optional: true - '@biomejs/cli-linux-x64@2.2.0': + '@biomejs/cli-linux-x64@2.2.4': optional: true - '@biomejs/cli-win32-arm64@2.2.0': + '@biomejs/cli-win32-arm64@2.2.4': optional: true - '@biomejs/cli-win32-x64@2.2.0': + '@biomejs/cli-win32-x64@2.2.4': optional: true '@esbuild/aix-ppc64@0.25.8': From a0b09438101993f8de5a51315c8522773a287a5e Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Tue, 30 Sep 2025 11:56:50 -0300 Subject: [PATCH 03/49] feat: bump version for inbound release (#642) Co-authored-by: Vitor Capretz --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 233f594c..8b3873d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.2", + "version": "6.2.0-canary.0", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From c0b1b4f57f4979bd6a0c1862be09ae5e09c724ef Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 1 Oct 2025 10:22:34 -0300 Subject: [PATCH 04/49] chore: bump to 6.2.0-canary.1 (#649) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b3873d4..34999dd3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.0-canary.0", + "version": "6.2.0-canary.1", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From e648bb915ddf85336dafadf5962c5b8ff609af59 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:28:19 +0000 Subject: [PATCH 05/49] chore(deps): update dependency @types/node to v22.18.8 (#638) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 34999dd3..6ef79f10 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@biomejs/biome": "2.2.4", - "@types/node": "22.18.6", + "@types/node": "22.18.8", "@types/react": "19.1.15", "pkg-pr-new": "0.0.60", "tsup": "8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03529899..7fd100b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@types/node': - specifier: 22.18.6 - version: 22.18.6 + specifier: 22.18.8 + version: 22.18.8 '@types/react': specifier: 19.1.15 version: 19.1.15 @@ -35,10 +35,10 @@ importers: version: 5.9.2 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@22.18.6)(yaml@2.8.1) + version: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) vitest-fetch-mock: specifier: 0.4.5 - version: 0.4.5(vitest@3.2.4(@types/node@22.18.6)(yaml@2.8.1)) + version: 0.4.5(vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1)) packages: @@ -734,8 +734,8 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/node@22.18.6': - resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==} + '@types/node@22.18.8': + resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} '@types/react@19.1.15': resolution: {integrity: sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==} @@ -1983,7 +1983,7 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@22.18.6': + '@types/node@22.18.8': dependencies: undici-types: 6.21.0 @@ -1999,13 +1999,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.1(@types/node@22.18.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.1(@types/node@22.18.6)(yaml@2.8.1) + vite: 7.1.1(@types/node@22.18.8)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2644,13 +2644,13 @@ snapshots: validate-npm-package-name@5.0.1: {} - vite-node@3.2.4(@types/node@22.18.6)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.18.8)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.5(@types/node@22.18.6)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.8)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2665,7 +2665,7 @@ snapshots: - tsx - yaml - vite@7.1.1(@types/node@22.18.6)(yaml@2.8.1): + vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2674,11 +2674,11 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 22.18.8 fsevents: 2.3.3 yaml: 2.8.1 - vite@7.1.5(@types/node@22.18.6)(yaml@2.8.1): + vite@7.1.5(@types/node@22.18.8)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2687,19 +2687,19 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 22.18.8 fsevents: 2.3.3 yaml: 2.8.1 - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/node@22.18.6)(yaml@2.8.1)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1)): dependencies: - vitest: 3.2.4(@types/node@22.18.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.18.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@22.18.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2717,11 +2717,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.1(@types/node@22.18.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.6)(yaml@2.8.1) + vite: 7.1.1(@types/node@22.18.8)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 22.18.8 transitivePeerDependencies: - jiti - less From 34b2267e3cfaad259d891cc179ca5dbe39cd6066 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:28:39 +0000 Subject: [PATCH 06/49] chore(deps): update dependency typescript to v5.9.3 (#645) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 6ef79f10..ab6c6847 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@types/react": "19.1.15", "pkg-pr-new": "0.0.60", "tsup": "8.5.0", - "typescript": "5.9.2", + "typescript": "5.9.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fd100b9..a1b95be5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,10 +29,10 @@ importers: version: 0.0.60 tsup: specifier: 8.5.0 - version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1) + version: 8.5.0(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: - specifier: 5.9.2 - version: 5.9.2 + specifier: 5.9.3 + version: 5.9.3 vitest: specifier: 3.2.4 version: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) @@ -1324,8 +1324,8 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -2587,7 +2587,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1): + tsup@8.5.0(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.8) cac: 6.7.14 @@ -2608,7 +2608,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.6 - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color @@ -2619,7 +2619,7 @@ snapshots: type-detect@4.1.0: {} - typescript@5.9.2: {} + typescript@5.9.3: {} ufo@1.6.1: {} From 98fa90c1994506413df5e1fe4ac5936d7ef03fb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:28:46 +0000 Subject: [PATCH 07/49] chore(deps): update tj-actions/changed-files digest to d6f020b (#651) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/preview-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 8514a474..fb77454b 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -30,7 +30,7 @@ jobs: - name: Find changed files id: changed_files if: github.event_name == 'pull_request' - uses: tj-actions/changed-files@212f9a7760ad2b8eb511185b841f3725a62c2ae0 + uses: tj-actions/changed-files@d6f020b1d9d7992dcf07f03b14d42832f866b495 with: files: src/**/* dir_names: true From 5d9d306c97cc19d0c1f0f80b6fd0930d5f2b1f15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:28:57 +0000 Subject: [PATCH 08/49] chore(deps): update pnpm to v10.18.0 (#653) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab6c6847..7038f7ed 100644 --- a/package.json +++ b/package.json @@ -64,5 +64,5 @@ "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, - "packageManager": "pnpm@10.17.1" + "packageManager": "pnpm@10.18.0" } From a5df0a530bdf37c96bd3b19758d0de99cc19c3b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:32:01 +0000 Subject: [PATCH 09/49] chore(deps): update dependency @biomejs/biome to v2.2.5 (#652) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 74 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 7038f7ed..6e49a82b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ } }, "devDependencies": { - "@biomejs/biome": "2.2.4", + "@biomejs/biome": "2.2.5", "@types/node": "22.18.8", "@types/react": "19.1.15", "pkg-pr-new": "0.0.60", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1b95be5..1a1d56f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: version: 1.76.1 devDependencies: '@biomejs/biome': - specifier: 2.2.4 - version: 2.2.4 + specifier: 2.2.5 + version: 2.2.5 '@types/node': specifier: 22.18.8 version: 22.18.8 @@ -54,55 +54,55 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} - '@biomejs/biome@2.2.4': - resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} + '@biomejs/biome@2.2.5': + resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.4': - resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} + '@biomejs/cli-darwin-arm64@2.2.5': + resolution: {integrity: sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.4': - resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} + '@biomejs/cli-darwin-x64@2.2.5': + resolution: {integrity: sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.4': - resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} + '@biomejs/cli-linux-arm64-musl@2.2.5': + resolution: {integrity: sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.4': - resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} + '@biomejs/cli-linux-arm64@2.2.5': + resolution: {integrity: sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.4': - resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} + '@biomejs/cli-linux-x64-musl@2.2.5': + resolution: {integrity: sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.4': - resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} + '@biomejs/cli-linux-x64@2.2.5': + resolution: {integrity: sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.4': - resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} + '@biomejs/cli-win32-arm64@2.2.5': + resolution: {integrity: sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.4': - resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} + '@biomejs/cli-win32-x64@2.2.5': + resolution: {integrity: sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1537,39 +1537,39 @@ snapshots: '@actions/io@1.1.3': {} - '@biomejs/biome@2.2.4': + '@biomejs/biome@2.2.5': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.4 - '@biomejs/cli-darwin-x64': 2.2.4 - '@biomejs/cli-linux-arm64': 2.2.4 - '@biomejs/cli-linux-arm64-musl': 2.2.4 - '@biomejs/cli-linux-x64': 2.2.4 - '@biomejs/cli-linux-x64-musl': 2.2.4 - '@biomejs/cli-win32-arm64': 2.2.4 - '@biomejs/cli-win32-x64': 2.2.4 + '@biomejs/cli-darwin-arm64': 2.2.5 + '@biomejs/cli-darwin-x64': 2.2.5 + '@biomejs/cli-linux-arm64': 2.2.5 + '@biomejs/cli-linux-arm64-musl': 2.2.5 + '@biomejs/cli-linux-x64': 2.2.5 + '@biomejs/cli-linux-x64-musl': 2.2.5 + '@biomejs/cli-win32-arm64': 2.2.5 + '@biomejs/cli-win32-x64': 2.2.5 - '@biomejs/cli-darwin-arm64@2.2.4': + '@biomejs/cli-darwin-arm64@2.2.5': optional: true - '@biomejs/cli-darwin-x64@2.2.4': + '@biomejs/cli-darwin-x64@2.2.5': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.4': + '@biomejs/cli-linux-arm64-musl@2.2.5': optional: true - '@biomejs/cli-linux-arm64@2.2.4': + '@biomejs/cli-linux-arm64@2.2.5': optional: true - '@biomejs/cli-linux-x64-musl@2.2.4': + '@biomejs/cli-linux-x64-musl@2.2.5': optional: true - '@biomejs/cli-linux-x64@2.2.4': + '@biomejs/cli-linux-x64@2.2.5': optional: true - '@biomejs/cli-win32-arm64@2.2.4': + '@biomejs/cli-win32-arm64@2.2.5': optional: true - '@biomejs/cli-win32-x64@2.2.4': + '@biomejs/cli-win32-x64@2.2.5': optional: true '@esbuild/aix-ppc64@0.25.8': From 9e4b04a8fac3a830d42fd05239481e4f5a3d2d1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:11:51 +0000 Subject: [PATCH 10/49] chore(deps): update dependency @types/react to v19.2.0 (#640) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6e49a82b..b7da9db3 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "devDependencies": { "@biomejs/biome": "2.2.5", "@types/node": "22.18.8", - "@types/react": "19.1.15", + "@types/react": "19.2.0", "pkg-pr-new": "0.0.60", "tsup": "8.5.0", "typescript": "5.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a1d56f6..a36c343b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ importers: specifier: 22.18.8 version: 22.18.8 '@types/react': - specifier: 19.1.15 - version: 19.1.15 + specifier: 19.2.0 + version: 19.2.0 pkg-pr-new: specifier: 0.0.60 version: 0.0.60 @@ -737,8 +737,8 @@ packages: '@types/node@22.18.8': resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} - '@types/react@19.1.15': - resolution: {integrity: sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==} + '@types/react@19.2.0': + resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -1987,7 +1987,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/react@19.1.15': + '@types/react@19.2.0': dependencies: csstype: 3.1.3 From 294aeba1749343f8c2174043724747f1641ca6f5 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Sat, 4 Oct 2025 19:09:11 -0300 Subject: [PATCH 11/49] feat: show headers in response (#657) --- src/inbound/inbound.spec.ts | 8 ++++++++ src/inbound/inbound.ts | 1 + src/inbound/interfaces/get-inbound-email.interface.ts | 1 + src/inbound/interfaces/inbound-email.ts | 1 + 4 files changed, 11 insertions(+) diff --git a/src/inbound/inbound.spec.ts b/src/inbound/inbound.spec.ts index 16366645..a50b5a0c 100644 --- a/src/inbound/inbound.spec.ts +++ b/src/inbound/inbound.spec.ts @@ -52,6 +52,9 @@ describe('Inbound', () => { bcc: null, cc: ['cc@example.com'], reply_to: ['reply@example.com'], + headers: { + example: 'value', + }, attachments: [ { id: 'att_123', @@ -93,6 +96,9 @@ describe('Inbound', () => { ], "createdAt": "2023-04-07T23:13:52.669661+00:00", "from": "sender@example.com", + "headers": { + "example": "value", + }, "html": "

hello world

", "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", "object": "inbound", @@ -123,6 +129,7 @@ describe('Inbound', () => { bcc: null, cc: null, reply_to: null, + headers: {}, attachments: [], }; @@ -146,6 +153,7 @@ describe('Inbound', () => { "cc": null, "createdAt": "2023-04-07T23:13:52.669661+00:00", "from": "sender@example.com", + "headers": {}, "html": null, "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", "object": "inbound", diff --git a/src/inbound/inbound.ts b/src/inbound/inbound.ts index df24cce1..9b551f42 100644 --- a/src/inbound/inbound.ts +++ b/src/inbound/inbound.ts @@ -35,6 +35,7 @@ export class Inbound { replyTo: apiResponse.reply_to, html: apiResponse.html, text: apiResponse.text, + headers: apiResponse.headers, attachments: apiResponse.attachments.map((attachment) => ({ id: attachment.id, filename: attachment.filename, diff --git a/src/inbound/interfaces/get-inbound-email.interface.ts b/src/inbound/interfaces/get-inbound-email.interface.ts index aef3ba2a..4b6ccc57 100644 --- a/src/inbound/interfaces/get-inbound-email.interface.ts +++ b/src/inbound/interfaces/get-inbound-email.interface.ts @@ -14,6 +14,7 @@ export interface GetInboundEmailApiResponse { reply_to: string[] | null; html: string | null; text: string | null; + headers: Record; attachments: Array<{ id: string; filename: string; diff --git a/src/inbound/interfaces/inbound-email.ts b/src/inbound/interfaces/inbound-email.ts index b00a4c13..0207ffb9 100644 --- a/src/inbound/interfaces/inbound-email.ts +++ b/src/inbound/interfaces/inbound-email.ts @@ -9,6 +9,7 @@ export interface InboundEmail { replyTo: string[] | null; html: string | null; text: string | null; + headers: Record; attachments: InboundEmailAttachment[]; } From bfb4026d6c6fc71494fb506f6b1a5d79891a7ecf Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Sat, 4 Oct 2025 19:15:15 -0300 Subject: [PATCH 12/49] chore: release new canary for headers (#658) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7da9db3..f156e2f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.0-canary.1", + "version": "6.2.0-canary.2", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From 2a5fd6d457f5a8fa97259102ed0f5cedf9d5dfae Mon Sep 17 00:00:00 2001 From: Ty Mick <5317080+TyMick@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:21:28 -0700 Subject: [PATCH 13/49] chore: improve PR title check error (#664) Co-authored-by: Gabriel Miranda --- .github/scripts/pr-title-check.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/scripts/pr-title-check.js b/.github/scripts/pr-title-check.js index 98674c50..f543032d 100644 --- a/.github/scripts/pr-title-check.js +++ b/.github/scripts/pr-title-check.js @@ -10,12 +10,10 @@ const isValidType = (title) => const validateTitle = (title) => { if (!isValidType(title)) { console.error( - `PR title does not follow the required format. - example: "type: My PR Title" - - - type: "feat", "fix", "chore", or "refactor" - - First letter of the PR title needs to be lowercased - `, + `PR title does not follow the required format "[type]: [title]". +- Example: "fix: email compatibility issue" +- Allowed types: 'feat', 'fix', 'chore', 'refactor' +- First letter of the title portion (after the colon) must be lowercased`, ); process.exit(1); } From 20220314844e99bec0335687f1066db9bb9dff81 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 16:55:46 -0300 Subject: [PATCH 14/49] feat: move emails inbound method to emails.receiving. (#666) --- src/emails/emails.ts | 7 ++- .../interfaces/get-inbound-email.interface.ts | 11 +--- src/emails/receiving/interfaces/index.ts | 1 + .../receiving/receiving.spec.ts} | 28 +++++----- src/emails/receiving/receiving.ts | 17 ++++++ src/inbound/inbound.ts | 53 ------------------- src/inbound/interfaces/inbound-email.ts | 22 -------- src/inbound/interfaces/index.ts | 5 -- src/index.ts | 2 +- src/resend.ts | 2 - 10 files changed, 41 insertions(+), 107 deletions(-) rename src/{inbound => emails/receiving}/interfaces/get-inbound-email.interface.ts (63%) create mode 100644 src/emails/receiving/interfaces/index.ts rename src/{inbound/inbound.spec.ts => emails/receiving/receiving.spec.ts} (86%) create mode 100644 src/emails/receiving/receiving.ts delete mode 100644 src/inbound/inbound.ts delete mode 100644 src/inbound/interfaces/inbound-email.ts delete mode 100644 src/inbound/interfaces/index.ts diff --git a/src/emails/emails.ts b/src/emails/emails.ts index 8be8b119..cd470a78 100644 --- a/src/emails/emails.ts +++ b/src/emails/emails.ts @@ -27,9 +27,14 @@ import type { UpdateEmailResponse, UpdateEmailResponseSuccess, } from './interfaces/update-email-options.interface'; +import { Receiving } from './receiving/receiving'; export class Emails { - constructor(private readonly resend: Resend) {} + readonly receiving: Receiving; + + constructor(private readonly resend: Resend) { + this.receiving = new Receiving(resend); + } async send( payload: CreateEmailOptions, diff --git a/src/inbound/interfaces/get-inbound-email.interface.ts b/src/emails/receiving/interfaces/get-inbound-email.interface.ts similarity index 63% rename from src/inbound/interfaces/get-inbound-email.interface.ts rename to src/emails/receiving/interfaces/get-inbound-email.interface.ts index 4b6ccc57..4e19c00d 100644 --- a/src/inbound/interfaces/get-inbound-email.interface.ts +++ b/src/emails/receiving/interfaces/get-inbound-email.interface.ts @@ -1,8 +1,6 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { InboundEmail } from './inbound-email'; +import type { ErrorResponse } from '../../../interfaces'; -// API response type (snake_case from API) -export interface GetInboundEmailApiResponse { +export interface GetInboundEmailResponseSuccess { object: 'inbound'; id: string; to: string[]; @@ -24,11 +22,6 @@ export interface GetInboundEmailApiResponse { }>; } -// SDK response type (camelCase for users) -export interface GetInboundEmailResponseSuccess extends InboundEmail { - object: 'inbound'; -} - export type GetInboundEmailResponse = | { data: GetInboundEmailResponseSuccess; diff --git a/src/emails/receiving/interfaces/index.ts b/src/emails/receiving/interfaces/index.ts new file mode 100644 index 00000000..7137669a --- /dev/null +++ b/src/emails/receiving/interfaces/index.ts @@ -0,0 +1 @@ +export * from './get-inbound-email.interface'; diff --git a/src/inbound/inbound.spec.ts b/src/emails/receiving/receiving.spec.ts similarity index 86% rename from src/inbound/inbound.spec.ts rename to src/emails/receiving/receiving.spec.ts index a50b5a0c..d89ec665 100644 --- a/src/inbound/inbound.spec.ts +++ b/src/emails/receiving/receiving.spec.ts @@ -1,9 +1,9 @@ -import type { ErrorResponse } from '../interfaces'; -import { Resend } from '../resend'; +import type { ErrorResponse } from '../../interfaces'; +import { Resend } from '../../resend'; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); -describe('Inbound', () => { +describe('Receiving', () => { afterEach(() => fetchMock.resetMocks()); describe('get', () => { @@ -22,7 +22,7 @@ describe('Inbound', () => { }, }); - const result = resend.inbound.get( + const result = resend.emails.receiving.get( '61cda979-919d-4b9d-9638-c148b93ff410', ); @@ -39,7 +39,7 @@ describe('Inbound', () => { }); describe('when inbound email found', () => { - it('returns inbound email with transformed fields', async () => { + it('returns inbound email', async () => { const apiResponse = { object: 'inbound' as const, id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', @@ -74,7 +74,7 @@ describe('Inbound', () => { }, }); - const result = await resend.inbound.get( + const result = await resend.emails.receiving.get( '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', ); @@ -83,9 +83,9 @@ describe('Inbound', () => { "data": { "attachments": [ { - "contentDisposition": "attachment", - "contentId": "cid_123", - "contentType": "application/pdf", + "content_disposition": "attachment", + "content_id": "cid_123", + "content_type": "application/pdf", "filename": "document.pdf", "id": "att_123", }, @@ -94,7 +94,7 @@ describe('Inbound', () => { "cc": [ "cc@example.com", ], - "createdAt": "2023-04-07T23:13:52.669661+00:00", + "created_at": "2023-04-07T23:13:52.669661+00:00", "from": "sender@example.com", "headers": { "example": "value", @@ -102,7 +102,7 @@ describe('Inbound', () => { "html": "

hello world

", "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", "object": "inbound", - "replyTo": [ + "reply_to": [ "reply@example.com", ], "subject": "Test inbound email", @@ -141,7 +141,7 @@ describe('Inbound', () => { }, }); - const result = await resend.inbound.get( + const result = await resend.emails.receiving.get( '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', ); @@ -151,13 +151,13 @@ describe('Inbound', () => { "attachments": [], "bcc": null, "cc": null, - "createdAt": "2023-04-07T23:13:52.669661+00:00", + "created_at": "2023-04-07T23:13:52.669661+00:00", "from": "sender@example.com", "headers": {}, "html": null, "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", "object": "inbound", - "replyTo": null, + "reply_to": null, "subject": "Test inbound email", "text": "hello world", "to": [ diff --git a/src/emails/receiving/receiving.ts b/src/emails/receiving/receiving.ts new file mode 100644 index 00000000..370161ce --- /dev/null +++ b/src/emails/receiving/receiving.ts @@ -0,0 +1,17 @@ +import type { Resend } from '../../resend'; +import type { + GetInboundEmailResponse, + GetInboundEmailResponseSuccess, +} from './interfaces/get-inbound-email.interface'; + +export class Receiving { + constructor(private readonly resend: Resend) {} + + async get(id: string): Promise { + const data = await this.resend.get( + `/emails/receiving/${id}`, + ); + + return data; + } +} diff --git a/src/inbound/inbound.ts b/src/inbound/inbound.ts deleted file mode 100644 index 9b551f42..00000000 --- a/src/inbound/inbound.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Resend } from '../resend'; -import type { - GetInboundEmailApiResponse, - GetInboundEmailResponse, - GetInboundEmailResponseSuccess, -} from './interfaces/get-inbound-email.interface'; - -export class Inbound { - constructor(private readonly resend: Resend) {} - - async get(id: string): Promise { - const data = await this.resend.get( - `/emails/inbound/${id}`, - ); - - if (data.error) { - return { - data: null, - error: data.error, - }; - } - - const apiResponse = data.data; - - // Transform snake_case to camelCase - const transformedData: GetInboundEmailResponseSuccess = { - object: apiResponse.object, - id: apiResponse.id, - to: apiResponse.to, - from: apiResponse.from, - createdAt: apiResponse.created_at, - subject: apiResponse.subject, - bcc: apiResponse.bcc, - cc: apiResponse.cc, - replyTo: apiResponse.reply_to, - html: apiResponse.html, - text: apiResponse.text, - headers: apiResponse.headers, - attachments: apiResponse.attachments.map((attachment) => ({ - id: attachment.id, - filename: attachment.filename, - contentType: attachment.content_type, - contentId: attachment.content_id, - contentDisposition: attachment.content_disposition, - })), - }; - - return { - data: transformedData, - error: null, - }; - } -} diff --git a/src/inbound/interfaces/inbound-email.ts b/src/inbound/interfaces/inbound-email.ts deleted file mode 100644 index 0207ffb9..00000000 --- a/src/inbound/interfaces/inbound-email.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface InboundEmail { - id: string; - to: string[]; - from: string; - createdAt: string; - subject: string; - bcc: string[] | null; - cc: string[] | null; - replyTo: string[] | null; - html: string | null; - text: string | null; - headers: Record; - attachments: InboundEmailAttachment[]; -} - -export interface InboundEmailAttachment { - id: string; - filename: string; - contentType: string; - contentId: string; - contentDisposition: string; -} diff --git a/src/inbound/interfaces/index.ts b/src/inbound/interfaces/index.ts deleted file mode 100644 index 75aeb194..00000000 --- a/src/inbound/interfaces/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { - GetInboundEmailResponse, - GetInboundEmailResponseSuccess, -} from './get-inbound-email.interface'; -export type { InboundEmail, InboundEmailAttachment } from './inbound-email'; diff --git a/src/index.ts b/src/index.ts index 64646851..a43e4bb5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,6 @@ export * from './common/interfaces'; export * from './contacts/interfaces'; export * from './domains/interfaces'; export * from './emails/interfaces'; -export * from './inbound/interfaces'; +export * from './emails/receiving/interfaces'; export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; diff --git a/src/resend.ts b/src/resend.ts index e4a08e61..9c6f97ef 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -10,7 +10,6 @@ import type { PatchOptions } from './common/interfaces/patch-option.interface'; import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; -import { Inbound } from './inbound/inbound'; import type { ErrorResponse } from './interfaces'; import { Webhooks } from './webhooks/webhooks'; @@ -37,7 +36,6 @@ export class Resend { readonly domains = new Domains(this); readonly emails = new Emails(this); readonly webhooks = new Webhooks(); - readonly inbound = new Inbound(this); constructor(readonly key?: string) { if (!key) { From 54569baca3328d0f0e7d3e8905617c43d689072f Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 16:57:27 -0300 Subject: [PATCH 15/49] feat: move attachment inbound methods to be nested (#667) --- src/attachments/attachments.ts | 82 +--------------- .../interfaces/list-attachments.interface.ts | 29 ------ .../{ => receiving}/interfaces/attachment.ts | 6 +- .../interfaces/get-attachment.interface.ts | 4 +- .../{ => receiving}/interfaces/index.ts | 0 .../interfaces/list-attachments.interface.ts | 21 ++++ .../receiving.spec.ts} | 97 ++++++------------- src/attachments/receiving/receiving.ts | 37 +++++++ src/index.ts | 2 +- 9 files changed, 98 insertions(+), 180 deletions(-) delete mode 100644 src/attachments/interfaces/list-attachments.interface.ts rename src/attachments/{ => receiving}/interfaces/attachment.ts (52%) rename src/attachments/{ => receiving}/interfaces/get-attachment.interface.ts (90%) rename src/attachments/{ => receiving}/interfaces/index.ts (100%) create mode 100644 src/attachments/receiving/interfaces/list-attachments.interface.ts rename src/attachments/{attachments.spec.ts => receiving/receiving.spec.ts} (72%) create mode 100644 src/attachments/receiving/receiving.ts diff --git a/src/attachments/attachments.ts b/src/attachments/attachments.ts index 7ac622f0..80e48f52 100644 --- a/src/attachments/attachments.ts +++ b/src/attachments/attachments.ts @@ -1,84 +1,10 @@ import type { Resend } from '../resend'; -import type { - GetAttachmentApiResponse, - GetAttachmentOptions, - GetAttachmentResponse, - GetAttachmentResponseSuccess, -} from './interfaces/get-attachment.interface'; -import type { - ListAttachmentsApiResponse, - ListAttachmentsOptions, - ListAttachmentsResponse, -} from './interfaces/list-attachments.interface'; +import { Receiving } from './receiving/receiving'; export class Attachments { - constructor(private readonly resend: Resend) {} + readonly receiving: Receiving; - async get(options: GetAttachmentOptions): Promise { - const { inboundId, id } = options; - - const data = await this.resend.get( - `/emails/inbound/${inboundId}/attachments/${id}`, - ); - - if (data.error) { - return { - data: null, - error: data.error, - }; - } - - const apiResponse = data.data; - - const transformedData: GetAttachmentResponseSuccess = { - object: apiResponse.object, - data: { - id: apiResponse.data.id, - filename: apiResponse.data.filename, - contentType: apiResponse.data.content_type, - contentDisposition: apiResponse.data.content_disposition, - contentId: apiResponse.data.content_id, - content: apiResponse.data.content, - }, - }; - - return { - data: transformedData, - error: null, - }; - } - - async list( - options: ListAttachmentsOptions, - ): Promise { - const { inboundId } = options; - - const data = await this.resend.get( - `/emails/inbound/${inboundId}/attachments`, - ); - - if (data.error) { - return { - data: null, - error: data.error, - }; - } - - const apiResponse = data.data; - - // Transform snake_case to camelCase and return array directly - const transformedData = apiResponse.data.map((attachment) => ({ - id: attachment.id, - filename: attachment.filename, - contentType: attachment.content_type, - contentDisposition: attachment.content_disposition, - contentId: attachment.content_id, - content: attachment.content, - })); - - return { - data: transformedData, - error: null, - }; + constructor(resend: Resend) { + this.receiving = new Receiving(resend); } } diff --git a/src/attachments/interfaces/list-attachments.interface.ts b/src/attachments/interfaces/list-attachments.interface.ts deleted file mode 100644 index 966c7a78..00000000 --- a/src/attachments/interfaces/list-attachments.interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { InboundAttachment } from './attachment'; - -export interface ListAttachmentsOptions { - inboundId: string; -} - -// API response type (snake_case from API) -export interface ListAttachmentsApiResponse { - object: 'attachment'; - data: Array<{ - id: string; - filename?: string; - content_type: string; - content_disposition: 'inline' | 'attachment'; - content_id?: string; - content: string; - }>; -} - -export type ListAttachmentsResponse = - | { - data: InboundAttachment[]; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/attachments/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts similarity index 52% rename from src/attachments/interfaces/attachment.ts rename to src/attachments/receiving/interfaces/attachment.ts index b59c1292..718868a6 100644 --- a/src/attachments/interfaces/attachment.ts +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -1,8 +1,8 @@ export interface InboundAttachment { id: string; filename?: string; - contentType: string; - contentDisposition: 'inline' | 'attachment'; - contentId?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; content: string; // base64 } diff --git a/src/attachments/interfaces/get-attachment.interface.ts b/src/attachments/receiving/interfaces/get-attachment.interface.ts similarity index 90% rename from src/attachments/interfaces/get-attachment.interface.ts rename to src/attachments/receiving/interfaces/get-attachment.interface.ts index 700c1c80..74901024 100644 --- a/src/attachments/interfaces/get-attachment.interface.ts +++ b/src/attachments/receiving/interfaces/get-attachment.interface.ts @@ -1,8 +1,8 @@ -import type { ErrorResponse } from '../../interfaces'; +import type { ErrorResponse } from '../../../interfaces'; import type { InboundAttachment } from './attachment'; export interface GetAttachmentOptions { - inboundId: string; + emailId: string; id: string; } diff --git a/src/attachments/interfaces/index.ts b/src/attachments/receiving/interfaces/index.ts similarity index 100% rename from src/attachments/interfaces/index.ts rename to src/attachments/receiving/interfaces/index.ts diff --git a/src/attachments/receiving/interfaces/list-attachments.interface.ts b/src/attachments/receiving/interfaces/list-attachments.interface.ts new file mode 100644 index 00000000..f4c29245 --- /dev/null +++ b/src/attachments/receiving/interfaces/list-attachments.interface.ts @@ -0,0 +1,21 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { InboundAttachment } from './attachment'; + +export interface ListAttachmentsOptions { + emailId: string; +} + +export interface ListAttachmentsResponseSuccess { + object: 'attachment'; + data: InboundAttachment[]; +} + +export type ListAttachmentsResponse = + | { + data: ListAttachmentsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/attachments/attachments.spec.ts b/src/attachments/receiving/receiving.spec.ts similarity index 72% rename from src/attachments/attachments.spec.ts rename to src/attachments/receiving/receiving.spec.ts index 50f1beec..7f773ad2 100644 --- a/src/attachments/attachments.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -1,9 +1,9 @@ -import type { ErrorResponse } from '../interfaces'; -import { Resend } from '../resend'; +import type { ErrorResponse } from '../../interfaces'; +import { Resend } from '../../resend'; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); -describe('Attachments', () => { +describe('Receiving', () => { afterEach(() => fetchMock.resetMocks()); describe('get', () => { @@ -22,8 +22,8 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.get({ - inboundId: '61cda979-919d-4b9d-9638-c148b93ff410', + const result = await resend.attachments.receiving.get({ + emailId: '61cda979-919d-4b9d-9638-c148b93ff410', id: 'att_123', }); @@ -61,8 +61,8 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.get({ - inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + const result = await resend.attachments.receiving.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_123', }); @@ -71,9 +71,9 @@ describe('Attachments', () => { "data": { "data": { "content": "base64encodedcontent==", - "contentDisposition": "attachment", - "contentId": "cid_123", - "contentType": "application/pdf", + "content_disposition": "attachment", + "content_id": "cid_123", + "content_type": "application/pdf", "filename": "document.pdf", "id": "att_123", }, @@ -106,8 +106,8 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.get({ - inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + const result = await resend.attachments.receiving.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_456', }); @@ -116,9 +116,9 @@ describe('Attachments', () => { "data": { "data": { "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", - "contentDisposition": "inline", - "contentId": "cid_456", - "contentType": "image/png", + "content_disposition": "inline", + "content_id": "cid_456", + "content_type": "image/png", "filename": "image.png", "id": "att_456", }, @@ -150,8 +150,8 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.get({ - inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + const result = await resend.attachments.receiving.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_789', }); @@ -160,10 +160,8 @@ describe('Attachments', () => { "data": { "data": { "content": "base64content", - "contentDisposition": "attachment", - "contentId": undefined, - "contentType": "text/plain", - "filename": undefined, + "content_disposition": "attachment", + "content_type": "text/plain", "id": "att_789", }, "object": "attachment", @@ -180,7 +178,7 @@ describe('Attachments', () => { it('returns error', async () => { const response: ErrorResponse = { name: 'not_found', - message: 'Inbound email not found', + message: 'Email not found', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -191,24 +189,16 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.list({ - inboundId: '61cda979-919d-4b9d-9638-c148b93ff410', + const result = await resend.attachments.receiving.list({ + emailId: '61cda979-919d-4b9d-9638-c148b93ff410', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": null, - "error": { - "message": "Inbound email not found", - "name": "not_found", - }, -} -`); + expect(result).toEqual({ data: null, error: response }); }); }); describe('when attachments found', () => { - it('returns multiple attachments with transformed fields', async () => { + it('returns multiple attachments', async () => { const apiResponse = { object: 'attachment' as const, data: [ @@ -239,33 +229,11 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.list({ - inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": [ - { - "content": "base64encodedcontent==", - "contentDisposition": "attachment", - "contentId": "cid_123", - "contentType": "application/pdf", - "filename": "document.pdf", - "id": "att_123", - }, - { - "content": "imagebase64==", - "contentDisposition": "inline", - "contentId": "cid_456", - "contentType": "image/png", - "filename": "image.png", - "id": "att_456", - }, - ], - "error": null, -} -`); + expect(result).toEqual({ data: apiResponse, error: null }); }); it('returns empty array when no attachments', async () => { @@ -282,16 +250,11 @@ describe('Attachments', () => { }, }); - const result = await resend.attachments.list({ - inboundId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": [], - "error": null, -} -`); + expect(result).toEqual({ data: apiResponse, error: null }); }); }); }); diff --git a/src/attachments/receiving/receiving.ts b/src/attachments/receiving/receiving.ts new file mode 100644 index 00000000..cb27b589 --- /dev/null +++ b/src/attachments/receiving/receiving.ts @@ -0,0 +1,37 @@ +import type { Resend } from '../../resend'; +import type { + GetAttachmentOptions, + GetAttachmentResponse, + GetAttachmentResponseSuccess, +} from './interfaces/get-attachment.interface'; +import type { + ListAttachmentsOptions, + ListAttachmentsResponse, + ListAttachmentsResponseSuccess, +} from './interfaces/list-attachments.interface'; + +export class Receiving { + constructor(private readonly resend: Resend) {} + + async get(options: GetAttachmentOptions): Promise { + const { emailId, id } = options; + + const data = await this.resend.get( + `/emails/inbound/${emailId}/attachments/${id}`, + ); + + return data; + } + + async list( + options: ListAttachmentsOptions, + ): Promise { + const { emailId } = options; + + const data = await this.resend.get( + `/emails/inbound/${emailId}/attachments`, + ); + + return data; + } +} diff --git a/src/index.ts b/src/index.ts index a43e4bb5..48ae9290 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './api-keys/interfaces'; -export * from './attachments/interfaces'; +export * from './attachments/receiving/interfaces'; export * from './audiences/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; From 236c533432d5c7a61f69133836a40177489b0ae6 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 17:31:56 -0300 Subject: [PATCH 16/49] feat: add inbound listing method (#668) --- src/emails/receiving/interfaces/index.ts | 1 + .../list-inbound-emails.interface.ts | 26 +++ src/emails/receiving/receiving.spec.ts | 220 +++++++++++++++++- src/emails/receiving/receiving.ts | 19 ++ 4 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/emails/receiving/interfaces/list-inbound-emails.interface.ts diff --git a/src/emails/receiving/interfaces/index.ts b/src/emails/receiving/interfaces/index.ts index 7137669a..3295255d 100644 --- a/src/emails/receiving/interfaces/index.ts +++ b/src/emails/receiving/interfaces/index.ts @@ -1 +1,2 @@ export * from './get-inbound-email.interface'; +export * from './list-inbound-emails.interface'; diff --git a/src/emails/receiving/interfaces/list-inbound-emails.interface.ts b/src/emails/receiving/interfaces/list-inbound-emails.interface.ts new file mode 100644 index 00000000..6550841c --- /dev/null +++ b/src/emails/receiving/interfaces/list-inbound-emails.interface.ts @@ -0,0 +1,26 @@ +import type { PaginationOptions } from '../../../common/interfaces'; +import type { ErrorResponse } from '../../../interfaces'; +import type { GetInboundEmailResponseSuccess } from './get-inbound-email.interface'; + +export type ListInboundEmailsOptions = PaginationOptions; + +export type ListInboundEmail = Omit< + GetInboundEmailResponseSuccess, + 'html' | 'text' | 'headers' | 'object' +>; + +export interface ListInboundEmailsResponseSuccess { + object: 'list'; + has_more: boolean; + data: ListInboundEmail[]; +} + +export type ListInboundEmailsResponse = + | { + data: ListInboundEmailsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/emails/receiving/receiving.spec.ts b/src/emails/receiving/receiving.spec.ts index d89ec665..f3542124 100644 --- a/src/emails/receiving/receiving.spec.ts +++ b/src/emails/receiving/receiving.spec.ts @@ -11,7 +11,7 @@ describe('Receiving', () => { it('returns error', async () => { const response: ErrorResponse = { name: 'not_found', - message: 'Inbound email not found', + message: 'Email not found', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -30,7 +30,7 @@ describe('Receiving', () => { { "data": null, "error": { - "message": "Inbound email not found", + "message": "Email not found", "name": "not_found", }, } @@ -170,4 +170,220 @@ describe('Receiving', () => { }); }); }); + + describe('list', () => { + describe('when no inbound emails found', () => { + it('returns empty list', async () => { + const response = { + object: 'list' as const, + has_more: false, + data: [], + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.emails.receiving.list(); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "data": [], + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + }); + + describe('when inbound emails found', () => { + it('returns list of inbound emails with transformed fields', async () => { + const apiResponse = { + object: 'list' as const, + has_more: true, + data: [ + { + id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + to: ['received@example.com'], + from: 'sender@example.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + subject: 'Test inbound email 1', + bcc: null, + cc: ['cc@example.com'], + reply_to: ['reply@example.com'], + attachments: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + size: 12345, + }, + ], + }, + { + id: '87e9bcdb-6b03-43e8-9ea0-1e7gffa19d00', + to: ['another@example.com'], + from: 'sender2@example.com', + created_at: '2023-04-08T10:20:30.123456+00:00', + subject: 'Test inbound email 2', + bcc: ['bcc@example.com'], + cc: null, + reply_to: null, + attachments: [], + }, + ], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.emails.receiving.list(); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "attachments": [ + { + "content_disposition": "attachment", + "content_id": "cid_123", + "content_type": "application/pdf", + "filename": "document.pdf", + "id": "att_123", + "size": 12345, + }, + ], + "bcc": null, + "cc": [ + "cc@example.com", + ], + "created_at": "2023-04-07T23:13:52.669661+00:00", + "from": "sender@example.com", + "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + "reply_to": [ + "reply@example.com", + ], + "subject": "Test inbound email 1", + "to": [ + "received@example.com", + ], + }, + { + "attachments": [], + "bcc": [ + "bcc@example.com", + ], + "cc": null, + "created_at": "2023-04-08T10:20:30.123456+00:00", + "from": "sender2@example.com", + "id": "87e9bcdb-6b03-43e8-9ea0-1e7gffa19d00", + "reply_to": null, + "subject": "Test inbound email 2", + "to": [ + "another@example.com", + ], + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, +} +`); + }); + + it('supports pagination with limit parameter', async () => { + const apiResponse = { + object: 'list' as const, + has_more: true, + data: [ + { + id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + to: ['received@example.com'], + from: 'sender@example.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + subject: 'Test inbound email', + bcc: null, + cc: null, + reply_to: null, + attachments: [], + }, + ], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + await resend.emails.receiving.list({ limit: 10 }); + + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving?limit=10', + ); + }); + + it('supports pagination with after parameter', async () => { + const apiResponse = { + object: 'list' as const, + has_more: false, + data: [], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + await resend.emails.receiving.list({ after: 'cursor123' }); + + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving?after=cursor123', + ); + }); + + it('supports pagination with before parameter', async () => { + const apiResponse = { + object: 'list' as const, + has_more: false, + data: [], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + await resend.emails.receiving.list({ before: 'cursor456' }); + + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving?before=cursor456', + ); + }); + }); + }); }); diff --git a/src/emails/receiving/receiving.ts b/src/emails/receiving/receiving.ts index 370161ce..8592827b 100644 --- a/src/emails/receiving/receiving.ts +++ b/src/emails/receiving/receiving.ts @@ -1,8 +1,14 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { GetInboundEmailResponse, GetInboundEmailResponseSuccess, } from './interfaces/get-inbound-email.interface'; +import type { + ListInboundEmailsOptions, + ListInboundEmailsResponse, + ListInboundEmailsResponseSuccess, +} from './interfaces/list-inbound-emails.interface'; export class Receiving { constructor(private readonly resend: Resend) {} @@ -14,4 +20,17 @@ export class Receiving { return data; } + + async list( + options: ListInboundEmailsOptions = {}, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/emails/receiving?${queryString}` + : '/emails/receiving'; + + const data = await this.resend.get(url); + + return data; + } } From 64a674feab58e9be922a37fb105d2ce2bdcaa8a2 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Fri, 10 Oct 2025 09:02:14 -0300 Subject: [PATCH 17/49] feat: add pagination for inbound email attachments (#670) --- .../interfaces/list-attachments.interface.ts | 8 +- src/attachments/receiving/receiving.spec.ts | 127 ++++++++++++++---- src/attachments/receiving/receiving.ts | 12 +- src/emails/receiving/receiving.spec.ts | 12 +- 4 files changed, 121 insertions(+), 38 deletions(-) diff --git a/src/attachments/receiving/interfaces/list-attachments.interface.ts b/src/attachments/receiving/interfaces/list-attachments.interface.ts index f4c29245..7d5dfdbd 100644 --- a/src/attachments/receiving/interfaces/list-attachments.interface.ts +++ b/src/attachments/receiving/interfaces/list-attachments.interface.ts @@ -1,12 +1,14 @@ +import type { PaginationOptions } from '../../../common/interfaces'; import type { ErrorResponse } from '../../../interfaces'; import type { InboundAttachment } from './attachment'; -export interface ListAttachmentsOptions { +export type ListAttachmentsOptions = PaginationOptions & { emailId: string; -} +}; export interface ListAttachmentsResponseSuccess { - object: 'attachment'; + object: 'list'; + has_more: boolean; data: InboundAttachment[]; } diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts index 7f773ad2..7ea6a63e 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -1,5 +1,8 @@ import type { ErrorResponse } from '../../interfaces'; import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { GetAttachmentResponseSuccess } from './interfaces'; +import type { ListAttachmentsResponseSuccess } from './interfaces/list-attachments.interface'; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -41,7 +44,7 @@ describe('Receiving', () => { describe('when attachment found', () => { it('returns attachment with transformed fields', async () => { - const apiResponse = { + const apiResponse: GetAttachmentResponseSuccess = { object: 'attachment' as const, data: { id: 'att_123', @@ -174,6 +177,33 @@ describe('Receiving', () => { }); describe('list', () => { + const apiResponse: ListAttachmentsResponseSuccess = { + object: 'list' as const, + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + content: 'base64encodedcontent==', + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + content: 'imagebase64==', + }, + ], + }; + + const headers = { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }; + describe('when inbound email not found', () => { it('returns error', async () => { const response: ErrorResponse = { @@ -199,28 +229,6 @@ describe('Receiving', () => { describe('when attachments found', () => { it('returns multiple attachments', async () => { - const apiResponse = { - object: 'attachment' as const, - data: [ - { - id: 'att_123', - filename: 'document.pdf', - content_type: 'application/pdf', - content_id: 'cid_123', - content_disposition: 'attachment' as const, - content: 'base64encodedcontent==', - }, - { - id: 'att_456', - filename: 'image.png', - content_type: 'image/png', - content_id: 'cid_456', - content_disposition: 'inline' as const, - content: 'imagebase64==', - }, - ], - }; - fetchMock.mockOnce(JSON.stringify(apiResponse), { status: 200, headers: { @@ -237,12 +245,12 @@ describe('Receiving', () => { }); it('returns empty array when no attachments', async () => { - const apiResponse = { + const emptyResponse = { object: 'attachment' as const, data: [], }; - fetchMock.mockOnce(JSON.stringify(apiResponse), { + fetchMock.mockOnce(JSON.stringify(emptyResponse), { status: 200, headers: { 'content-type': 'application/json', @@ -254,7 +262,74 @@ describe('Receiving', () => { emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); - expect(result).toEqual({ data: apiResponse, error: null }); + expect(result).toEqual({ data: emptyResponse, error: null }); + }); + }); + + describe('when no pagination options provided', () => { + it('calls endpoint without query params and return the response', async () => { + mockSuccessResponse(apiResponse, { + headers, + }); + + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + ); + }); + }); + + describe('when pagination options are provided', () => { + it('calls endpoint passing limit param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + limit: 10, + }); + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?limit=10', + ); + }); + + it('calls endpoint passing after param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + after: 'cursor123', + }); + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?after=cursor123', + ); + }); + + it('calls endpoint passing before param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + const result = await resend.attachments.receiving.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + before: 'cursor123', + }); + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?before=cursor123', + ); }); }); }); diff --git a/src/attachments/receiving/receiving.ts b/src/attachments/receiving/receiving.ts index cb27b589..f13748f8 100644 --- a/src/attachments/receiving/receiving.ts +++ b/src/attachments/receiving/receiving.ts @@ -1,3 +1,4 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { GetAttachmentOptions, @@ -17,7 +18,7 @@ export class Receiving { const { emailId, id } = options; const data = await this.resend.get( - `/emails/inbound/${emailId}/attachments/${id}`, + `/emails/receiving/${emailId}/attachments/${id}`, ); return data; @@ -28,9 +29,12 @@ export class Receiving { ): Promise { const { emailId } = options; - const data = await this.resend.get( - `/emails/inbound/${emailId}/attachments`, - ); + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/emails/receiving/${emailId}/attachments?${queryString}` + : `/emails/receiving/${emailId}/attachments`; + + const data = await this.resend.get(url); return data; } diff --git a/src/emails/receiving/receiving.spec.ts b/src/emails/receiving/receiving.spec.ts index f3542124..20c4ec42 100644 --- a/src/emails/receiving/receiving.spec.ts +++ b/src/emails/receiving/receiving.spec.ts @@ -1,5 +1,9 @@ import type { ErrorResponse } from '../../interfaces'; import { Resend } from '../../resend'; +import type { + GetInboundEmailResponseSuccess, + ListInboundEmailsResponseSuccess, +} from './interfaces'; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -40,7 +44,7 @@ describe('Receiving', () => { describe('when inbound email found', () => { it('returns inbound email', async () => { - const apiResponse = { + const apiResponse: GetInboundEmailResponseSuccess = { object: 'inbound' as const, id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', to: ['received@example.com'], @@ -117,7 +121,7 @@ describe('Receiving', () => { }); it('returns inbound email with no attachments', async () => { - const apiResponse = { + const apiResponse: GetInboundEmailResponseSuccess = { object: 'inbound' as const, id: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', to: ['received@example.com'], @@ -205,7 +209,7 @@ describe('Receiving', () => { describe('when inbound emails found', () => { it('returns list of inbound emails with transformed fields', async () => { - const apiResponse = { + const apiResponse: ListInboundEmailsResponseSuccess = { object: 'list' as const, has_more: true, data: [ @@ -225,7 +229,6 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment' as const, - size: 12345, }, ], }, @@ -265,7 +268,6 @@ describe('Receiving', () => { "content_type": "application/pdf", "filename": "document.pdf", "id": "att_123", - "size": 12345, }, ], "bcc": null, From ffe4fce482def02043066a127137cb3e99b06506 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:59:04 -0300 Subject: [PATCH 18/49] chore(deps): update pnpm to v10.18.2 (#659) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f156e2f8..1a12186b 100644 --- a/package.json +++ b/package.json @@ -64,5 +64,5 @@ "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, - "packageManager": "pnpm@10.18.0" + "packageManager": "pnpm@10.18.2" } From d4e34eef62315550cff806f362f89fe92f0aa45a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:59:11 -0300 Subject: [PATCH 19/49] chore(deps): update dependency @types/node to v22.18.9 (#669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 1a12186b..81d83a53 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@biomejs/biome": "2.2.5", - "@types/node": "22.18.8", + "@types/node": "22.18.9", "@types/react": "19.2.0", "pkg-pr-new": "0.0.60", "tsup": "8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a36c343b..ca8b869a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: 2.2.5 version: 2.2.5 '@types/node': - specifier: 22.18.8 - version: 22.18.8 + specifier: 22.18.9 + version: 22.18.9 '@types/react': specifier: 19.2.0 version: 19.2.0 @@ -35,10 +35,10 @@ importers: version: 5.9.3 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) + version: 3.2.4(@types/node@22.18.9)(yaml@2.8.1) vitest-fetch-mock: specifier: 0.4.5 - version: 0.4.5(vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1)) + version: 0.4.5(vitest@3.2.4(@types/node@22.18.9)(yaml@2.8.1)) packages: @@ -734,8 +734,8 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/node@22.18.8': - resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} + '@types/node@22.18.9': + resolution: {integrity: sha512-5yBtK0k/q8PjkMXbTfeIEP/XVYnz1R9qZJ3yUicdEW7ppdDJfe+MqXEhpqDL3mtn4Wvs1u0KLEG0RXzCgNpsSg==} '@types/react@19.2.0': resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} @@ -1983,7 +1983,7 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@22.18.8': + '@types/node@22.18.9': dependencies: undici-types: 6.21.0 @@ -1999,13 +1999,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.1(@types/node@22.18.9)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.1(@types/node@22.18.8)(yaml@2.8.1) + vite: 7.1.1(@types/node@22.18.9)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2644,13 +2644,13 @@ snapshots: validate-npm-package-name@5.0.1: {} - vite-node@3.2.4(@types/node@22.18.8)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.18.9)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.5(@types/node@22.18.8)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.9)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2665,7 +2665,7 @@ snapshots: - tsx - yaml - vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1): + vite@7.1.1(@types/node@22.18.9)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2674,11 +2674,11 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.8 + '@types/node': 22.18.9 fsevents: 2.3.3 yaml: 2.8.1 - vite@7.1.5(@types/node@22.18.8)(yaml@2.8.1): + vite@7.1.5(@types/node@22.18.9)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2687,19 +2687,19 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.8 + '@types/node': 22.18.9 fsevents: 2.3.3 yaml: 2.8.1 - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/node@22.18.9)(yaml@2.8.1)): dependencies: - vitest: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.18.9)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.18.8)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.18.9)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@22.18.8)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@22.18.9)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2717,11 +2717,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.1(@types/node@22.18.8)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.8)(yaml@2.8.1) + vite: 7.1.1(@types/node@22.18.9)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.9)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.18.8 + '@types/node': 22.18.9 transitivePeerDependencies: - jiti - less From 798d80ba41d970044d7bbb3c64c4c68d787d3870 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:59:18 -0300 Subject: [PATCH 20/49] chore(deps): update pnpm/action-setup digest to 41ff726 (#665) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 881ab003..95095c55 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - name: pnpm setup - uses: pnpm/action-setup@f2b2b233b538f500472c7274c7012f57857d8ce0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 - name: Install packages run: pnpm install - name: Run Lint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 064231b9..48a3869d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - name: pnpm setup - uses: pnpm/action-setup@f2b2b233b538f500472c7274c7012f57857d8ce0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 - name: Install packages run: pnpm install - name: Run Tests From 41b7f7b48fb97a28bab270b32257f8b813692d3a Mon Sep 17 00:00:00 2001 From: Ty Mick <5317080+TyMick@users.noreply.github.com> Date: Tue, 14 Oct 2025 06:18:28 -0700 Subject: [PATCH 21/49] feat: partial (API Keys, Audiences, and Contacts) non-mocked test coverage (#663) --- .gitignore | 1 + package.json | 10 +- pnpm-lock.yaml | 926 +++++++++++++++- .../recording.har | 229 ++++ .../recording.har | 229 ++++ .../recording.har | 229 ++++ .../recording.har | 229 ++++ .../recording.har | 121 +++ src/api-keys/api-keys.integration.spec.ts | 84 ++ src/api-keys/api-keys.spec.ts | 7 + src/attachments/receiving/receiving.spec.ts | 5 + .../recording.har | 229 ++++ .../recording.har | 122 +++ .../recording.har | 336 ++++++ .../recording.har | 121 +++ .../recording.har | 121 +++ .../recording.har | 336 ++++++ src/audiences/audiences.integration.spec.ts | 151 +++ src/audiences/audiences.spec.ts | 5 + src/batch/batch.spec.ts | 5 + src/broadcasts/broadcasts.spec.ts | 5 + .../recording.har | 337 ++++++ .../recording.har | 122 +++ .../recording.har | 444 ++++++++ .../recording.har | 444 ++++++++ .../recording.har | 336 ++++++ .../recording.har | 989 ++++++++++++++++++ .../recording.har | 984 +++++++++++++++++ .../recording.har | 336 ++++++ .../recording.har | 551 ++++++++++ .../recording.har | 551 ++++++++++ .../recording.har | 556 ++++++++++ src/contacts/contacts.integration.spec.ts | 326 ++++++ src/contacts/contacts.spec.ts | 5 + src/domains/domains.spec.ts | 5 + src/emails/emails.spec.ts | 5 + src/emails/receiving/receiving.spec.ts | 4 + src/test-utils/polly-setup.ts | 68 ++ vitest.config.mts | 9 + vitest.setup.mts | 10 +- 40 files changed, 9576 insertions(+), 7 deletions(-) create mode 100644 src/api-keys/__recordings__/API-Keys-Integration-Tests-create-allows-creating-an-API-key-with-an-empty-name_1578522646/recording.har create mode 100644 src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-full-access_2925126786/recording.har create mode 100644 src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-sending-access_198781637/recording.har create mode 100644 src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-removes-an-API-key_2317016229/recording.har create mode 100644 src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-returns-error-for-non-existent-API-key_2163054369/recording.har create mode 100644 src/api-keys/api-keys.integration.spec.ts create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har create mode 100644 src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har create mode 100644 src/audiences/audiences.integration.spec.ts create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har create mode 100644 src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har create mode 100644 src/contacts/contacts.integration.spec.ts create mode 100644 src/test-utils/polly-setup.ts diff --git a/.gitignore b/.gitignore index 63520da7..bbed7ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist build +.env.test diff --git a/package.json b/package.json index 81d83a53..65c6fd05 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "lint": "biome check .", "prepublishOnly": "pnpm run build", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:record": "rimraf --glob \"**/__recordings__\" && cross-env TEST_MODE=record vitest run", + "test:dev": "cross-env TEST_MODE=dev vitest run" }, "repository": { "type": "git", @@ -56,9 +58,15 @@ }, "devDependencies": { "@biomejs/biome": "2.2.5", + "@pollyjs/adapter-fetch": "6.0.7", + "@pollyjs/core": "6.0.6", + "@pollyjs/persister-fs": "6.0.6", "@types/node": "22.18.9", "@types/react": "19.2.0", + "cross-env": "10.1.0", + "dotenv": "17.2.3", "pkg-pr-new": "0.0.60", + "rimraf": "6.0.1", "tsup": "8.5.0", "typescript": "5.9.3", "vitest": "3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca8b869a..74190eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,15 +18,33 @@ importers: '@biomejs/biome': specifier: 2.2.5 version: 2.2.5 + '@pollyjs/adapter-fetch': + specifier: 6.0.7 + version: 6.0.7 + '@pollyjs/core': + specifier: 6.0.6 + version: 6.0.6 + '@pollyjs/persister-fs': + specifier: 6.0.6 + version: 6.0.6 '@types/node': specifier: 22.18.9 version: 22.18.9 '@types/react': specifier: 19.2.0 version: 19.2.0 + cross-env: + specifier: 10.1.0 + version: 10.1.0 + dotenv: + specifier: 17.2.3 + version: 17.2.3 pkg-pr-new: specifier: 0.0.60 version: 0.0.60 + rimraf: + specifier: 6.0.1 + version: 6.0.1 tsup: specifier: 8.5.0 version: 8.5.0(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) @@ -107,6 +125,9 @@ packages: cpu: [x64] os: [win32] + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} @@ -423,6 +444,14 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -507,6 +536,27 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pollyjs/adapter-fetch@6.0.7': + resolution: {integrity: sha512-kv44DROx/2qzlcgS71EccGr2/I5nK40Xt92paGNI+1/Kmz290bw/ykt8cvXDg4O4xCc9Fh/jXeAkS7qwGpCx2g==} + + '@pollyjs/adapter@6.0.6': + resolution: {integrity: sha512-szhys0NiFQqCJDMC0kpDyjhLqSI7aWc6m6iATCRKgcMcN/7QN85pb3GmRzvnNV8+/Bi2AUSCwxZljcsKhbYVWQ==} + + '@pollyjs/core@6.0.6': + resolution: {integrity: sha512-1ZZcmojW8iSFmvHGeLlvuudM3WiDV842FsVvtPAo3HoAYE6jCNveLHJ+X4qvonL4enj1SyTF3hXA107UkQFQrA==} + + '@pollyjs/node-server@6.0.6': + resolution: {integrity: sha512-nkP1+hdNoVOlrRz9R84haXVsaSmo8Xmq7uYK9GeUMSLQy4Fs55ZZ9o2KI6vRA8F6ZqJSbC31xxwwIoTkjyP7Vg==} + + '@pollyjs/persister-fs@6.0.6': + resolution: {integrity: sha512-/ALVgZiH2zGqwLkW0Mntc0Oq1v7tR8LS8JD2SAyIsHpnSXeBUnfPWwjAuYw0vqORHFVEbwned6MBRFfvU/3qng==} + + '@pollyjs/persister@6.0.6': + resolution: {integrity: sha512-9KB1p+frvYvFGur4ifzLnFKFLXAMXrhAhCnVhTnkG2WIqqQPT7y+mKBV/DKCmYFx8GPA9FiNGqt2pB53uJpIdw==} + + '@pollyjs/utils@6.0.6': + resolution: {integrity: sha512-nhVJoI3nRgRimE0V2DVSvsXXNROUH6iyJbroDu4IdsOIOFC1Ds0w+ANMB4NMwFaqE+AisWOmXFzwAGdAfyiQVg==} + '@react-email/render@1.2.3': resolution: {integrity: sha512-qu3XYNkHGao3teJexVD5CrcgFkNLrzbZvpZN17a7EyQYUN3kHkTkE9saqY4VbvGx6QoNU3p8rsk/Xm++D/+pTw==} engines: {node: '>=18.0.0'} @@ -722,6 +772,10 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sindresorhus/fnv1a@2.0.1': + resolution: {integrity: sha512-suq9tRQ6bkpMukTG5K5z0sPWB7t0zExMzZCdmYm6xTSSIm/yCKNm7VCL36wVeyTsFr597/UhU1OAYdHGMDiHrw==} + engines: {node: '>=10'} + '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -740,6 +794,9 @@ packages: '@types/react@19.2.0': resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -769,6 +826,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -793,6 +854,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -800,9 +864,23 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -812,10 +890,22 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -849,6 +939,30 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -856,6 +970,14 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -886,9 +1008,17 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -902,22 +1032,53 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -931,16 +1092,30 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -957,6 +1132,10 @@ packages: resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} engines: {node: '>=14.16'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -964,15 +1143,58 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -980,10 +1202,33 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-graceful-shutdown@3.1.14: + resolution: {integrity: sha512-aTbGAZDUtRt7gRmU+li7rt5WbJeemULZHLNrycJ1dRBU80Giut6NvzG8h5u1TW1zGHXkPGpEtoEKhPKogIRKdA==} + engines: {node: '>=4.0.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-absolute-url@3.0.3: + resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==} + engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -998,6 +1243,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1005,6 +1254,9 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -1019,21 +1271,64 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1048,6 +1343,13 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1059,10 +1361,34 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + nocache@3.0.4: + resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} + engines: {node: '>=12.0.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1072,6 +1398,10 @@ packages: parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1080,6 +1410,13 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1135,10 +1472,22 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + query-registry@3.0.1: resolution: {integrity: sha512-M9RxRITi2mHMVPU5zysNjctUT8bAPx6ltEXo/ir9+qmiM47Y7f0Ir3+OxUO5OjYAWdicBQRew7RtHtqUXydqlg==} engines: {node: '>=20'} @@ -1154,6 +1503,14 @@ packages: resolution: {integrity: sha512-Pzd/4IFnTb8E+I1P5rbLQoqpUHcXKg48qTYKi4EANg+sTPwGFEMOcYGiiZz6xuQcOMZP7MPsrdAPx+16Q8qahg==} engines: {node: '>=18'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -1177,6 +1534,11 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + rollup@4.46.2: resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1187,12 +1549,38 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + route-recognizer@0.3.4: + resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1201,6 +1589,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1208,6 +1612,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1224,6 +1632,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -1287,6 +1699,13 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1324,6 +1743,10 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1346,6 +1769,14 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + url-join@5.0.0: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1353,6 +1784,13 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -1361,6 +1799,10 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1572,6 +2014,8 @@ snapshots: '@biomejs/cli-win32-x64@2.2.5': optional: true + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.25.8': optional: true @@ -1730,6 +2174,12 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1837,6 +2287,63 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pollyjs/adapter-fetch@6.0.7': + dependencies: + '@pollyjs/adapter': 6.0.6 + '@pollyjs/utils': 6.0.6 + to-arraybuffer: 1.0.1 + + '@pollyjs/adapter@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + + '@pollyjs/core@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + '@sindresorhus/fnv1a': 2.0.1 + blueimp-md5: 2.19.0 + fast-json-stable-stringify: 2.1.0 + is-absolute-url: 3.0.3 + lodash-es: 4.17.21 + loglevel: 1.9.2 + route-recognizer: 0.3.4 + slugify: 1.6.6 + + '@pollyjs/node-server@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + body-parser: 1.20.3 + cors: 2.8.5 + express: 4.21.2 + fs-extra: 10.1.0 + http-graceful-shutdown: 3.1.14 + morgan: 1.10.1 + nocache: 3.0.4 + transitivePeerDependencies: + - supports-color + + '@pollyjs/persister-fs@6.0.6': + dependencies: + '@pollyjs/node-server': 6.0.6 + '@pollyjs/persister': 6.0.6 + transitivePeerDependencies: + - supports-color + + '@pollyjs/persister@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + '@types/set-cookie-parser': 2.4.10 + bowser: 2.12.1 + fast-json-stable-stringify: 2.1.0 + lodash-es: 4.17.21 + set-cookie-parser: 2.7.1 + utf8-byte-length: 1.0.5 + + '@pollyjs/utils@6.0.6': + dependencies: + qs: 6.14.0 + url-parse: 1.5.10 + '@react-email/render@1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: html-to-text: 9.0.5 @@ -1973,6 +2480,8 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sindresorhus/fnv1a@2.0.1': {} + '@stablelib/base64@1.0.1': {} '@types/chai@5.2.2': @@ -1991,6 +2500,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 22.18.9 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -2033,6 +2546,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn@8.15.0: {} ansi-regex@5.0.1: {} @@ -2047,12 +2565,39 @@ snapshots: any-promise@1.3.0: {} + array-flatten@1.1.1: {} + assertion-error@2.0.1: {} balanced-match@1.0.2: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + before-after-hook@2.2.3: {} + blueimp-md5@2.19.0: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bowser@2.12.1: {} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2062,8 +2607,20 @@ snapshots: esbuild: 0.25.8 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} chai@5.3.3: @@ -2092,6 +2649,26 @@ snapshots: consola@3.4.2: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2100,6 +2677,10 @@ snapshots: csstype@3.1.3: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2114,8 +2695,12 @@ snapshots: deepmerge@4.3.1: {} + depd@2.0.0: {} + deprecation@2.3.1: {} + destroy@1.2.0: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -2134,16 +2719,38 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + entities@4.5.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es6-promise@4.2.8: {} esbuild@0.25.8: @@ -2204,14 +2811,56 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + expect-type@1.2.2: {} + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@2.0.1: {} + fast-json-stable-stringify@2.1.0: {} + fast-sha256@1.3.0: {} fdir@6.5.0(picomatch@4.0.3): @@ -2220,6 +2869,18 @@ snapshots: filter-obj@5.1.0: {} + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.17 @@ -2231,9 +2892,39 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -2243,6 +2934,25 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -2258,8 +2968,32 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-graceful-shutdown@3.1.14: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-absolute-url@3.0.3: {} + is-fullwidth-code-point@3.0.0: {} isbinaryfile@5.0.6: {} @@ -2272,10 +3006,20 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + joycon@3.1.1: {} js-tokens@9.0.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + leac@0.6.0: {} lilconfig@3.1.3: {} @@ -2284,12 +3028,18 @@ snapshots: load-tsconfig@0.2.5: {} + lodash-es@4.17.21: {} + lodash.sortby@4.7.0: {} + loglevel@1.9.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 @@ -2298,6 +3048,26 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -2318,6 +3088,18 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + ms@2.1.3: {} mz@2.7.0: @@ -2328,8 +3110,24 @@ snapshots: nanoid@3.3.11: {} + negotiator@0.6.3: {} + + nocache@3.0.4: {} + object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -2341,6 +3139,8 @@ snapshots: leac: 0.6.0 peberminta: 0.9.0 + parseurl@1.3.3: {} + path-key@3.1.1: {} path-scurry@1.11.1: @@ -2348,6 +3148,13 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2392,8 +3199,21 @@ snapshots: prettier@3.6.2: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + query-registry@3.0.1: dependencies: query-string: 9.3.0 @@ -2413,6 +3233,15 @@ snapshots: quick-lru@7.1.0: {} + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -2430,6 +3259,11 @@ snapshots: resolve-from@5.0.0: {} + rimraf@6.0.1: + dependencies: + glob: 11.0.3 + package-json-from-dist: 1.0.1 + rollup@4.46.2: dependencies: '@types/estree': 1.0.8 @@ -2483,22 +3317,91 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.50.2 fsevents: 2.3.3 + route-recognizer@0.3.4: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + scheduler@0.26.0: {} selderee@0.11.0: dependencies: parseley: 0.12.1 + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.1: {} + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} + slugify@1.6.6: {} + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -2509,6 +3412,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.9.0: {} string-argv@0.3.2: {} @@ -2550,7 +3455,7 @@ snapshots: svix@1.76.1: dependencies: '@stablelib/base64': 1.0.1 - '@types/node': 22.18.6 + '@types/node': 22.18.9 es6-promise: 4.2.8 fast-sha256: 1.3.0 url-parse: 1.5.10 @@ -2579,6 +3484,10 @@ snapshots: tinyspy@4.0.3: {} + to-arraybuffer@1.0.1: {} + + toidentifier@1.0.1: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2619,6 +3528,11 @@ snapshots: type-detect@4.1.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + typescript@5.9.3: {} ufo@1.6.1: {} @@ -2633,6 +3547,10 @@ snapshots: universal-user-agent@6.0.1: {} + universalify@2.0.1: {} + + unpipe@1.0.0: {} + url-join@5.0.0: {} url-parse@1.5.10: @@ -2640,10 +3558,16 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + utf8-byte-length@1.0.5: {} + + utils-merge@1.0.1: {} + uuid@10.0.0: {} validate-npm-package-name@5.0.1: {} + vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.18.9)(yaml@2.8.1): dependencies: cac: 6.7.14 diff --git a/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-allows-creating-an-API-key-with-an-empty-name_1578522646/recording.har b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-allows-creating-an-API-key-with-an-empty-name_1578522646/recording.har new file mode 100644 index 00000000..1060a0f4 --- /dev/null +++ b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-allows-creating-an-API-key-with-an-empty-name_1578522646/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "API Keys Integration Tests > create > allows creating an API key with an empty name", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "08e45cbee1a68cd6c50beacab63ff057", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 11, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"\"}" + }, + "queryString": [], + "url": "https://api.resend.com/api-keys" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"id\":\"29b93cc6-1ca3-4733-8e64-cae19780862f\",\"object\":\"list\",\"token\":\"re_REDACTED_API_KEY\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285dffb08eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:02 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-gRGSXeJ1NgQrkIf9bPKYMYWykiU\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:24:01.871Z", + "time": 125, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 125 + } + }, + { + "_id": "71d647522c6be8a167d989fb62c324d8", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/api-keys/29b93cc6-1ca3-4733-8e64-cae19780862f" + }, + "response": { + "bodySize": 76, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 76, + "text": "{\"object\":\"list\",\"id\":\"29b93cc6-1ca3-4733-8e64-cae19780862f\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285e48ed0eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:02 GMT" + }, + { + "name": "etag", + "value": "W/\"4c-yk4rxjm2/9a3ewUWaHq1l1ySMQE\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:24:02.599Z", + "time": 138, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 138 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-full-access_2925126786/recording.har b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-full-access_2925126786/recording.har new file mode 100644 index 00000000..5b7adf08 --- /dev/null +++ b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-full-access_2925126786/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "API Keys Integration Tests > create > creates an API key with full access", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "198a1d2479615ecf7495e1413a777c74", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 58, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Integration Test Key\",\"permission\":\"full_access\"}" + }, + "queryString": [], + "url": "https://api.resend.com/api-keys" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"id\":\"690bd6a5-bbaf-4a7a-b379-8c883409de8c\",\"object\":\"list\",\"token\":\"re_REDACTED_API_KEY\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285cd7ff6eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:59 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-RZPLOwIDFhfvliqGMu8VCai1QqI\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:58.863Z", + "time": 172, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 172 + } + }, + { + "_id": "2bc5700d706513035ae27479d6ec6767", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/api-keys/690bd6a5-bbaf-4a7a-b379-8c883409de8c" + }, + "response": { + "bodySize": 76, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 76, + "text": "{\"object\":\"list\",\"id\":\"690bd6a5-bbaf-4a7a-b379-8c883409de8c\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285d209cdeb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:59 GMT" + }, + { + "name": "etag", + "value": "W/\"4c-tbhpwhD93PKjvi8JyXLo1NIVWU8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:59.639Z", + "time": 142, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 142 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-sending-access_198781637/recording.har b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-sending-access_198781637/recording.har new file mode 100644 index 00000000..f8f40471 --- /dev/null +++ b/src/api-keys/__recordings__/API-Keys-Integration-Tests-create-creates-an-API-key-with-sending-access_198781637/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "API Keys Integration Tests > create > creates an API key with sending access", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "d3e4e4c44144a13ecf13a0f5d1108670", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 69, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Integration Test Sending Key\",\"permission\":\"sending_access\"}" + }, + "queryString": [], + "url": "https://api.resend.com/api-keys" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"id\":\"02e47a5e-7f28-4160-9727-3e94b76bc500\",\"object\":\"list\",\"token\":\"re_REDACTED_API_KEY\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285d6ccaceb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:00 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-ydVg3V3fIed3KNJhk0w1jJ7VAEk\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:24:00.392Z", + "time": 132, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 132 + } + }, + { + "_id": "98e80477145aa5298b1981fff9ad8943", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/api-keys/02e47a5e-7f28-4160-9727-3e94b76bc500" + }, + "response": { + "bodySize": 76, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 76, + "text": "{\"object\":\"list\",\"id\":\"02e47a5e-7f28-4160-9727-3e94b76bc500\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285db5fc4eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:01 GMT" + }, + { + "name": "etag", + "value": "W/\"4c-YaVdiAnOP2T6rQNkwfC54ECUn4k\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:24:01.125Z", + "time": 125, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 125 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-removes-an-API-key_2317016229/recording.har b/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-removes-an-API-key_2317016229/recording.har new file mode 100644 index 00000000..4d944ec3 --- /dev/null +++ b/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-removes-an-API-key_2317016229/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "API Keys Integration Tests > remove > removes an API key", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "05263730684c691490237a1b79c06f65", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 41, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Integration Test Key to Remove\"}" + }, + "queryString": [], + "url": "https://api.resend.com/api-keys" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"id\":\"77a60b3f-9567-48fd-a43b-172a2924414c\",\"object\":\"list\",\"token\":\"re_REDACTED_API_KEY\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285e93a19eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:03 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-sVlFL3EgFTe7qcvu5MpcTkIbyxo\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:24:03.344Z", + "time": 115, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 115 + } + }, + { + "_id": "27f23d182b524509f709b55ab0f284bf", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/api-keys/77a60b3f-9567-48fd-a43b-172a2924414c" + }, + "response": { + "bodySize": 76, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 76, + "text": "{\"object\":\"list\",\"id\":\"77a60b3f-9567-48fd-a43b-172a2924414c\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285edad25eb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:04 GMT" + }, + { + "name": "etag", + "value": "W/\"4c-TP+KlL5zrGh22EVZJllaM5L3Aw4\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:24:04.062Z", + "time": 115, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 115 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-returns-error-for-non-existent-API-key_2163054369/recording.har b/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-returns-error-for-non-existent-API-key_2163054369/recording.har new file mode 100644 index 00000000..9c27a20d --- /dev/null +++ b/src/api-keys/__recordings__/API-Keys-Integration-Tests-remove-returns-error-for-non-existent-API-key_2163054369/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "API Keys Integration Tests > remove > returns error for non-existent API key", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "ec8b8412f62ae15fc83675f6e4d42f25", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/api-keys/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 67, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 67, + "text": "{\"statusCode\":404,\"message\":\"API key not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285f22fcaeb3d-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:24:04 GMT" + }, + { + "name": "etag", + "value": "W/\"43-W4pDo57J7V5dLLhL3pDbczLeGBU\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:24:04.783Z", + "time": 122, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 122 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/api-keys/api-keys.integration.spec.ts b/src/api-keys/api-keys.integration.spec.ts new file mode 100644 index 00000000..9e2eb503 --- /dev/null +++ b/src/api-keys/api-keys.integration.spec.ts @@ -0,0 +1,84 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ +import type { Polly } from '@pollyjs/core'; +import { Resend } from '../resend'; +import { setupPolly } from '../test-utils/polly-setup'; + +describe('API Keys Integration Tests', () => { + let polly: Polly; + let resend: Resend; + + beforeEach(() => { + polly = setupPolly(); + resend = new Resend(process.env.RESEND_API_KEY || 're_fake_key'); + }); + + afterEach(async () => { + await polly.stop(); + }); + + describe('create', () => { + it('creates an API key with full access', async () => { + const result = await resend.apiKeys.create({ + name: 'Integration Test Key', + permission: 'full_access', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.token).toBeTruthy(); + const keyId = result.data!.id; + + const removeResult = await resend.apiKeys.remove(keyId); + expect(removeResult.data).toBeTruthy(); + }); + + it('creates an API key with sending access', async () => { + const result = await resend.apiKeys.create({ + name: 'Integration Test Sending Key', + permission: 'sending_access', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.token).toBeTruthy(); + const keyId = result.data!.id; + + const removeResult = await resend.apiKeys.remove(keyId); + expect(removeResult.data).toBeTruthy(); + }); + + it('allows creating an API key with an empty name', async () => { + const result = await resend.apiKeys.create({ + name: '', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.token).toBeTruthy(); + const keyId = result.data!.id; + + const removeResult = await resend.apiKeys.remove(keyId); + expect(removeResult.data).toBeTruthy(); + }); + }); + + describe('remove', () => { + it('removes an API key', async () => { + const createResult = await resend.apiKeys.create({ + name: 'Integration Test Key to Remove', + }); + + expect(createResult.data?.id).toBeTruthy(); + const keyId = createResult.data!.id; + + const removeResult = await resend.apiKeys.remove(keyId); + + expect(removeResult.data).toBeTruthy(); + }); + + it('returns error for non-existent API key', async () => { + const result = await resend.apiKeys.remove( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.error?.name).toBe('not_found'); + }); + }); +}); diff --git a/src/api-keys/api-keys.spec.ts b/src/api-keys/api-keys.spec.ts index 2da88b91..6aece257 100644 --- a/src/api-keys/api-keys.spec.ts +++ b/src/api-keys/api-keys.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -8,7 +9,13 @@ import type { import type { ListApiKeysResponseSuccess } from './interfaces/list-api-keys.interface'; import type { RemoveApiKeyResponseSuccess } from './interfaces/remove-api-keys.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + describe('API Keys', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + describe('create', () => { it('creates an api key', async () => { const payload: CreateApiKeyOptions = { diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts index 7ea6a63e..d42c2b45 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -1,13 +1,18 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../../interfaces'; import { Resend } from '../../resend'; import { mockSuccessResponse } from '../../test-utils/mock-fetch'; import type { GetAttachmentResponseSuccess } from './interfaces'; import type { ListAttachmentsResponseSuccess } from './interfaces/list-attachments.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); describe('Receiving', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('get', () => { describe('when attachment not found', () => { diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har new file mode 100644 index 00000000..54fee951 --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > create > creates an audience", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "2a189e724feca29e3c1a8056e5428d06", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 24, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Audience\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 88, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 88, + "text": "{\"object\":\"audience\",\"id\":\"65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c\",\"name\":\"Test Audience\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2855c886b495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "88" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:41 GMT" + }, + { + "name": "etag", + "value": "W/\"58-tTHeOTSope7Hsdv56qpKbSlizRE\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:40.799Z", + "time": 588, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 588 + } + }, + { + "_id": "5efc9196e5d1c9496521f0439692d11a", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28563bbaa495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:42 GMT" + }, + { + "name": "etag", + "value": "W/\"50-ML1zUev5dxBL/DI0b7r5R7dExg0\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:41.989Z", + "time": 522, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 522 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har new file mode 100644 index 00000000..081b4cd9 --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har @@ -0,0 +1,122 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > create > handles validation errors", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "b40ca24e4da48cf9c42eec8d4ee8fd07", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"statusCode\":422,\"message\":\"Missing `name` field.\",\"name\":\"missing_required_field\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2856adf09495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "84" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:43 GMT" + }, + { + "name": "etag", + "value": "W/\"54-b7tWVBvPczzJWDVqTkO4kHnV3MM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-08T03:23:43.124Z", + "time": 124, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 124 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har new file mode 100644 index 00000000..5d391fb5 --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > get > retrieves an audience by id", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "71e47db40a081d9cea9d924be3674468", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 32, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Audience for Get\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 96, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 96, + "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"name\":\"Test Audience for Get\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2856f6abf495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "96" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:44 GMT" + }, + { + "name": "etag", + "value": "W/\"60-2qNyMV2yRuemKADjxL9CSP83RUw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:43.857Z", + "time": 192, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 192 + } + }, + { + "_id": "48fa447339cb9fc7aa29a6bc77974fce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 218, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/b9aad85e-2b5a-42ed-bc13-487395888501" + }, + "response": { + "bodySize": 141, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 141, + "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"name\":\"Test Audience for Get\",\"created_at\":\"2025-10-08 03:23:43.967943+00\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285745e9e495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:44 GMT" + }, + { + "name": "etag", + "value": "W/\"8d-SNoapenSoTRhpYIVLTXBnNdI9RA\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:44.653Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + }, + { + "_id": "15398319a047512af2d8b99cd06f4116", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/b9aad85e-2b5a-42ed-bc13-487395888501" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28578f9d4495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:45 GMT" + }, + { + "name": "etag", + "value": "W/\"50-vJVsF7PasUJ/5roQJaMWmZEz4Jw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:45.390Z", + "time": 210, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 210 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har new file mode 100644 index 00000000..4986385f --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > get > returns error for non-existent audience", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "37fe2d2726c58aab7b978ed64c0e5629", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 218, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2857e1dcc495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:46 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:23:46.209Z", + "time": 119, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 119 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har new file mode 100644 index 00000000..378f17ef --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > remove > appears to remove an audience that never existed", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "acc6807b398db55c7faded600d19fa59", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28591eedb495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:49 GMT" + }, + { + "name": "etag", + "value": "W/\"50-qtTbv74eHSLU1m48Aah48skg91s\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:49.377Z", + "time": 306, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 306 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har b/src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har new file mode 100644 index 00000000..6661c047 --- /dev/null +++ b/src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Audiences Integration Tests > remove > removes an audience", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "69ad88bb02d46c714f3985b02ea225e7", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 34, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Audience to Remove\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 98, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 98, + "text": "{\"object\":\"audience\",\"id\":\"adee0536-bff3-4d0c-8e8b-aa9c4d7603ad\",\"name\":\"Test Audience to Remove\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28582a8e3495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "98" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:47 GMT" + }, + { + "name": "etag", + "value": "W/\"62-hrZl7qEd/u74uUck14lbdJuEH8Y\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:46.937Z", + "time": 123, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 123 + } + }, + { + "_id": "8d200690d13a16dd7d02225e2fcd6ea8", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/adee0536-bff3-4d0c-8e8b-aa9c4d7603ad" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"adee0536-bff3-4d0c-8e8b-aa9c4d7603ad\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b285873c22495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:47 GMT" + }, + { + "name": "etag", + "value": "W/\"50-RmTgR3D92HzLSC3lrlZGJzC/Ec8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:47.663Z", + "time": 380, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 380 + } + }, + { + "_id": "1fccbbd4bbefafb9777178ba8efaa09b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 218, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/adee0536-bff3-4d0c-8e8b-aa9c4d7603ad" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2858d4bc0495b-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:48 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:23:48.646Z", + "time": 118, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 118 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/audiences.integration.spec.ts b/src/audiences/audiences.integration.spec.ts new file mode 100644 index 00000000..f5b39b66 --- /dev/null +++ b/src/audiences/audiences.integration.spec.ts @@ -0,0 +1,151 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ +import type { Polly } from '@pollyjs/core'; +import { Resend } from '../resend'; +import { setupPolly } from '../test-utils/polly-setup'; + +describe('Audiences Integration Tests', () => { + let polly: Polly; + let resend: Resend; + + beforeEach(() => { + polly = setupPolly(); + resend = new Resend(process.env.RESEND_API_KEY || 're_fake_key'); + }); + + afterEach(async () => { + await polly.stop(); + }); + + describe('create', () => { + it('creates an audience', async () => { + const result = await resend.audiences.create({ + name: 'Test Audience', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.name).toBeTruthy(); + expect(result.data?.object).toBe('audience'); + const audienceId = result.data!.id; + + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + }); + + it('handles validation errors', async () => { + // @ts-expect-error: Testing invalid input + const result = await resend.audiences.create({}); + + expect(result.error?.name).toBe('missing_required_field'); + }); + }); + + // Needs to be run with an account that can have multiple audiences + describe.todo('list', () => { + it('lists audiences without pagination', async () => { + const audienceIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.audiences.create({ + name: `Test audience ${i} for listing`, + }); + + expect(createResult.data?.id).toBeTruthy(); + audienceIds.push(createResult.data!.id); + } + + const result = await resend.audiences.list(); + + expect(result.data?.object).toBe('list'); + expect(result.data?.data.length).toBeGreaterThanOrEqual(6); + expect(result.data?.has_more).toBe(false); + } finally { + for (const id of audienceIds) { + const removeResult = await resend.audiences.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + + it('lists audiences with limit', async () => { + const audienceIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.audiences.create({ + name: `Test audience ${i} for listing with limit`, + }); + + expect(createResult.data?.id).toBeTruthy(); + audienceIds.push(createResult.data!.id); + } + + const result = await resend.audiences.list({ limit: 5 }); + + expect(result.data?.data.length).toBe(5); + expect(result.data?.has_more).toBe(true); + } finally { + for (const id of audienceIds) { + const removeResult = await resend.audiences.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + }); + + describe('get', () => { + it('retrieves an audience by id', async () => { + const createResult = await resend.audiences.create({ + name: 'Test Audience for Get', + }); + + expect(createResult.data?.id).toBeTruthy(); + const audienceId = createResult.data!.id; + + try { + const getResult = await resend.audiences.get(audienceId); + + expect(getResult.data?.id).toBe(audienceId); + expect(getResult.data?.name).toBe('Test Audience for Get'); + expect(getResult.data?.object).toBe('audience'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('returns error for non-existent audience', async () => { + const result = await resend.audiences.get( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.error?.name).toBe('not_found'); + }); + }); + + describe('remove', () => { + it('removes an audience', async () => { + const createResult = await resend.audiences.create({ + name: 'Test Audience to Remove', + }); + + expect(createResult.data?.id).toBeTruthy(); + const audienceId = createResult.data!.id; + + const removeResult = await resend.audiences.remove(audienceId); + + expect(removeResult.data?.deleted).toBe(true); + + const getResult = await resend.audiences.get(audienceId); + expect(getResult.error?.name).toBe('not_found'); + }); + + it('appears to remove an audience that never existed', async () => { + const result = await resend.audiences.remove( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.data?.deleted).toBe(true); + }); + }); +}); diff --git a/src/audiences/audiences.spec.ts b/src/audiences/audiences.spec.ts index 1ff01153..8bf771d4 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/audiences/audiences.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -9,8 +10,12 @@ import type { GetAudienceResponseSuccess } from './interfaces/get-audience.inter import type { ListAudiencesResponseSuccess } from './interfaces/list-audiences.interface'; import type { RemoveAudiencesResponseSuccess } from './interfaces/remove-audience.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + describe('Audiences', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('creates a audience', async () => { diff --git a/src/batch/batch.spec.ts b/src/batch/batch.spec.ts index 219dc662..71cce07a 100644 --- a/src/batch/batch.spec.ts +++ b/src/batch/batch.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import { Resend } from '../resend'; import { mockSuccessResponse, @@ -5,10 +6,14 @@ import { } from '../test-utils/mock-fetch'; import type { CreateBatchOptions } from './interfaces/create-batch-options.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); describe('Batch', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('sends multiple emails', async () => { diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index feca37ed..24c61204 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -10,10 +11,14 @@ import type { ListBroadcastsResponseSuccess } from './interfaces/list-broadcasts import type { RemoveBroadcastResponseSuccess } from './interfaces/remove-broadcast.interface'; import type { UpdateBroadcastResponseSuccess } from './interfaces/update-broadcast.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); describe('Broadcasts', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('missing `from`', async () => { diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har new file mode 100644 index 00000000..9c020abb --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har @@ -0,0 +1,337 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > create > creates a contact", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "148c961dbf5fba70f1daa20e3380c096", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 45, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for contact creation\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 109, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 109, + "text": "{\"object\":\"audience\",\"id\":\"cc41cb0c-d74b-48cc-8829-82b7235f9480\",\"name\":\"Test audience for contact creation\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283c70e53d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "109" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:36 GMT" + }, + { + "name": "etag", + "value": "W/\"6d-M3BoflhMDHltiRoXrhX6Fs8zP+c\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:35.841Z", + "time": 527, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 527 + } + }, + { + "_id": "eebc33c2f1c289ef85b370c244579564", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 67, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test@example.com\",\"first_name\":\"Test\",\"last_name\":\"User\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/cc41cb0c-d74b-48cc-8829-82b7235f9480/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"95f4dd7b-661b-4d83-b00c-4161de562b45\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283cd6c68d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:37 GMT" + }, + { + "name": "etag", + "value": "W/\"40-fOl6HaVbASpgeEMhS6Wn+WCb01I\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:36.972Z", + "time": 950, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 950 + } + }, + { + "_id": "12d49bd04054ef1bcd6baaab1a54534b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/cc41cb0c-d74b-48cc-8829-82b7235f9480" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"cc41cb0c-d74b-48cc-8829-82b7235f9480\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283d71db7d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:38 GMT" + }, + { + "name": "etag", + "value": "W/\"50-aO5SoYBXdozT9SbLcSJaAxEXeVM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:38.528Z", + "time": 296, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 296 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har new file mode 100644 index 00000000..df1329b3 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har @@ -0,0 +1,122 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > create > handles validation errors", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "8ceccdf09452d6948d99963948117790", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 201, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/undefined/contacts" + }, + "response": { + "bodySize": 87, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 87, + "text": "{\"statusCode\":422,\"message\":\"The `id` must be a valid UUID.\",\"name\":\"validation_error\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283dccbb4d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "87" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:39 GMT" + }, + { + "name": "etag", + "value": "W/\"57-lOl5qyYLjWNv3C4rGuvDsfdJanQ\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-08T03:22:39.437Z", + "time": 129, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 129 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har new file mode 100644 index 00000000..a528bbaf --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har @@ -0,0 +1,444 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > get > retrieves a contact by email", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "df079127264d81dedd58026a51f771f5", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 41, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for get by email\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 105, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 105, + "text": "{\"object\":\"audience\",\"id\":\"789c8f84-d880-4cc8-85c6-1d1a07c20d42\",\"name\":\"Test audience for get by email\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28456792ad3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "105" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:59 GMT" + }, + { + "name": "etag", + "value": "W/\"69-tvCZvE/vttZEA21HdBaZ1lmqgmw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:58.909Z", + "time": 183, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 183 + } + }, + { + "_id": "950f83a9b2efb96a89c93aca35e2336e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 41, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test-get-by-email@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"f314b366-6c97-4e6d-abda-d667a69364ff\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2845b5de4d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:59 GMT" + }, + { + "name": "etag", + "value": "W/\"40-42DGnIHcUqOQ5FXY5Qj0qGaAbKQ\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:59.694Z", + "time": 236, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 236 + } + }, + { + "_id": "f402678be2de96dd7ddf838dd2075b1c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 257, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42/contacts/test-get-by-email@example.com" + }, + "response": { + "bodySize": 205, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 205, + "text": "{\"object\":\"contact\",\"id\":\"f314b366-6c97-4e6d-abda-d667a69364ff\",\"email\":\"test-get-by-email@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 23:09:05.075871+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28460aaf1d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:00 GMT" + }, + { + "name": "etag", + "value": "W/\"cd-3BWf4FmU/U0yarVYZXcpkdxvq44\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:00.533Z", + "time": 131, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 131 + } + }, + { + "_id": "07318a530c6056325fcbe71f649bd4e8", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"789c8f84-d880-4cc8-85c6-1d1a07c20d42\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284653fc3d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:01 GMT" + }, + { + "name": "etag", + "value": "W/\"50-VbukeU9VzqKG4Zik8wmhPWliXaA\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:01.266Z", + "time": 217, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 217 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har new file mode 100644 index 00000000..b06d0d4e --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har @@ -0,0 +1,444 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > get > retrieves a contact by id", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "fc71545e2243811f6588a5e3a0e1f326", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 38, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for get by ID\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 102, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 102, + "text": "{\"object\":\"audience\",\"id\":\"73ce0954-bc04-4656-bba4-e054600715ca\",\"name\":\"Test audience for get by ID\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28441ed7bd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "102" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:55 GMT" + }, + { + "name": "etag", + "value": "W/\"66-KjuWnS1+tZJtY0w8MIEKMl7PFpg\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:55.606Z", + "time": 210, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 210 + } + }, + { + "_id": "a1c8b9e95dce5349cac37153431bde84", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 38, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test-get-by-id@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"e260e001-cee2-4f2f-aa76-ba59e3d3b3b5\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28446eaded3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:56 GMT" + }, + { + "name": "etag", + "value": "W/\"40-sQeLQX2QW71rx/jFhozcwHDP/Wc\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:56.418Z", + "time": 320, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 320 + } + }, + { + "_id": "2c30b18767715249d23abd8a3409dca8", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca/contacts/e260e001-cee2-4f2f-aa76-ba59e3d3b3b5" + }, + "response": { + "bodySize": 201, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 201, + "text": "{\"object\":\"contact\",\"id\":\"e260e001-cee2-4f2f-aa76-ba59e3d3b3b5\",\"email\":\"test-get-by-id@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:57:07.19709+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2844ca85ed3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:57 GMT" + }, + { + "name": "etag", + "value": "W/\"c9-A1lZp26OVe28XOSbNYeCZSybgL0\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:57.341Z", + "time": 126, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 126 + } + }, + { + "_id": "6fce7e499edf8238e08d21bcc068d443", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"73ce0954-bc04-4656-bba4-e054600715ca\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284513c9ad3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:58 GMT" + }, + { + "name": "etag", + "value": "W/\"50-VAI2qkfcbIxTFjKbVkH99ebetpE\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:58.068Z", + "time": 237, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 237 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har new file mode 100644 index 00000000..d8404360 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > get > returns error for non-existent contact", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "a6f6a6bc9a75be64a528f2fb082fbfc9", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 49, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for non-existent contact\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 113, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 113, + "text": "{\"object\":\"audience\",\"id\":\"65bd037b-8048-448d-b9d8-153200097147\",\"name\":\"Test audience for non-existent contact\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2846a5cf1d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "113" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:02 GMT" + }, + { + "name": "etag", + "value": "W/\"71-XMmvodXAYDCobiSEj7raOLn2pP8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:02.088Z", + "time": 138, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 138 + } + }, + { + "_id": "51154edc062160334d2522051a872777", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/65bd037b-8048-448d-b9d8-153200097147/contacts/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 67, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 67, + "text": "{\"statusCode\":404,\"message\":\"Contact not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2846ef958d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:03 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:23:02.829Z", + "time": 149, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 149 + } + }, + { + "_id": "426d3a38aac2c1de69a2b64f363dfef1", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/65bd037b-8048-448d-b9d8-153200097147" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"65bd037b-8048-448d-b9d8-153200097147\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28473ae55d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:03 GMT" + }, + { + "name": "etag", + "value": "W/\"50-jhMkz52LmanenOQbO761s7adex4\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:03.582Z", + "time": 247, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 247 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har new file mode 100644 index 00000000..4cfc7723 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har @@ -0,0 +1,989 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > list > lists contacts with limit", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "0b85ff30f3862f9132a01f3b54d39212", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 47, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for listing with limit\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 111, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 111, + "text": "{\"object\":\"audience\",\"id\":\"7152409b-1eb4-4300-96e8-f55e8df14c79\",\"name\":\"Test audience for listing with limit\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28411dda7d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "111" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:48 GMT" + }, + { + "name": "etag", + "value": "W/\"6f-yKILtqrdEbOctn20S2IgrMIXIqY\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:47.926Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + }, + { + "_id": "31ae6961ba3b639fa5adc520f5c09e3e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.0@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284166914d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:48 GMT" + }, + { + "name": "etag", + "value": "W/\"40-WcJL2Wka/X7v1Zq/rmJSt4fqpc8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:48.661Z", + "time": 295, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 295 + } + }, + { + "_id": "39bed937d9a89b4745d85b1a187a17a3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.1@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2841c0dc5d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:49 GMT" + }, + { + "name": "etag", + "value": "W/\"40-r0L5/NhwefMWKUlhD621pE9LFsw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:49.559Z", + "time": 318, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 318 + } + }, + { + "_id": "f87da3869230f7aec485e05f9b12cf8f", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.2@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28421ca5cd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:50 GMT" + }, + { + "name": "etag", + "value": "W/\"40-DR4PbWnf0QFpFaMfKVAi4SYFl6Q\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:50.479Z", + "time": 321, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 321 + } + }, + { + "_id": "5418916ba0532e75d9397fd441feca1d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.3@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284278ec6d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:51 GMT" + }, + { + "name": "etag", + "value": "W/\"40-SBZrC974KMJ39cUl1r0nnWVOHUE\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:51.403Z", + "time": 317, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 317 + } + }, + { + "_id": "6b100f0a4f9d1a2db01733f0f19f8d9d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.4@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2842d4b2bd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:52 GMT" + }, + { + "name": "etag", + "value": "W/\"40-49pb2Z2Gv139gDXRXffI9uLXccU\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:52.323Z", + "time": 219, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 219 + } + }, + { + "_id": "1aaae92fb32a5ce9e90b6ab71a756a24", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.5@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284326fb5d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:53 GMT" + }, + { + "name": "etag", + "value": "W/\"40-GK3+3/XcpMXwQrYY64qiPOWcO98\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:53.144Z", + "time": 248, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 248 + } + }, + { + "_id": "24bbd56ea637eb4ae9ebf6bd589d0edf", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 235, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [ + { + "name": "limit", + "value": "5" + } + ], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts?limit=5" + }, + "response": { + "bodySize": 920, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 920, + "text": "{\"object\":\"list\",\"has_more\":true,\"data\":[{\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:18.61266+00\",\"unsubscribed\":false},{\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.851518+00\",\"unsubscribed\":false},{\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.13096+00\",\"unsubscribed\":false},{\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:16.309282+00\",\"unsubscribed\":false},{\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:52.186404+00\",\"unsubscribed\":false}]}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28437cc6ad3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:54 GMT" + }, + { + "name": "etag", + "value": "W/\"398-fW0/lfZUazCpcdYtrpd+E01wfeo\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 368, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:53.999Z", + "time": 138, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 138 + } + }, + { + "_id": "97f58aacbb5130621d9879553f478803", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"7152409b-1eb4-4300-96e8-f55e8df14c79\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2843c78c4d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:54 GMT" + }, + { + "name": "etag", + "value": "W/\"50-sDJF/j+U5l3uff5MTKIEZmQXyCQ\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:54.741Z", + "time": 259, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 259 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har new file mode 100644 index 00000000..80b6cddf --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har @@ -0,0 +1,984 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > list > lists contacts without pagination", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "5b20e71185afda27e38dc3d9b7f57624", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 36, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for listing\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 100, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 100, + "text": "{\"object\":\"audience\",\"id\":\"148a671b-3445-4d29-a79f-50e78b4b24ee\",\"name\":\"Test audience for listing\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283e1687dd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "100" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:40 GMT" + }, + { + "name": "etag", + "value": "W/\"64-AfDuQHSdpSb+3bU91Tphho+O/mk\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:40.178Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + }, + { + "_id": "e05dc1b2c3bcd17accf3d9b3bff793fd", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.0@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283e5fce5d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:41 GMT" + }, + { + "name": "etag", + "value": "W/\"40-WcJL2Wka/X7v1Zq/rmJSt4fqpc8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:40.914Z", + "time": 362, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 362 + } + }, + { + "_id": "6446b997bf20c810f28c6bd2e0d32b45", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.1@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283ec0af5d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:42 GMT" + }, + { + "name": "etag", + "value": "W/\"40-r0L5/NhwefMWKUlhD621pE9LFsw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:41.880Z", + "time": 256, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 256 + } + }, + { + "_id": "c818eda960f70164cbc07cb1cf5984d2", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.2@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283f1691fd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:43 GMT" + }, + { + "name": "etag", + "value": "W/\"40-DR4PbWnf0QFpFaMfKVAi4SYFl6Q\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:42.738Z", + "time": 279, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 279 + } + }, + { + "_id": "eb6c676c26223b34b9d520fd76539570", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.3@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283f6ee1fd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:43 GMT" + }, + { + "name": "etag", + "value": "W/\"40-SBZrC974KMJ39cUl1r0nnWVOHUE\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:43.620Z", + "time": 265, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 265 + } + }, + { + "_id": "6b66b521adca931136b05dcb99eefb5d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.4@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b283fc5a96d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:44 GMT" + }, + { + "name": "etag", + "value": "W/\"40-49pb2Z2Gv139gDXRXffI9uLXccU\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:44.489Z", + "time": 269, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 269 + } + }, + { + "_id": "184123983e7f24eafe5bba4b05c4b19d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 30, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test.5@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28401cf49d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:45 GMT" + }, + { + "name": "etag", + "value": "W/\"40-GK3+3/XcpMXwQrYY64qiPOWcO98\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:22:45.361Z", + "time": 259, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 259 + } + }, + { + "_id": "1dcc6abb6ca116e4a00e870a2ff65ebd", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 227, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + }, + "response": { + "bodySize": 1097, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 1097, + "text": "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:18.61266+00\",\"unsubscribed\":false},{\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.851518+00\",\"unsubscribed\":false},{\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.13096+00\",\"unsubscribed\":false},{\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:16.309282+00\",\"unsubscribed\":false},{\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:52.186404+00\",\"unsubscribed\":false},{\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\",\"email\":\"test.0@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:51.231744+00\",\"unsubscribed\":false}]}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284073b9ed3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:46 GMT" + }, + { + "name": "etag", + "value": "W/\"449-d7s8QRhoXU5ObVQnddszfMAKzgk\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 368, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:46.223Z", + "time": 129, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 129 + } + }, + { + "_id": "691bff31ec52e8b2802ce08750907042", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"148a671b-3445-4d29-a79f-50e78b4b24ee\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2840bc807d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:22:47 GMT" + }, + { + "name": "etag", + "value": "W/\"50-qA69IAti3hYt4F1qPM4W79EcnF8\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:22:46.957Z", + "time": 362, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 362 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har new file mode 100644 index 00000000..eb346658 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > remove > appears to remove a contact that never existed", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "1c0a4af7474576bd5c1779e8e4af0cac", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 48, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for non-existent delete\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 112, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 112, + "text": "{\"object\":\"audience\",\"id\":\"82ef6091-9a99-4687-8207-e20a65b9caa0\",\"name\":\"Test audience for non-existent delete\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284c7aa29d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "112" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:17 GMT" + }, + { + "name": "etag", + "value": "W/\"70-WGLvJbhrmbGv4qRnzhIVzB1MNqk\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:17.015Z", + "time": 126, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 126 + } + }, + { + "_id": "a19c42f86ca0928e2ef08a45d696e70b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 267, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/82ef6091-9a99-4687-8207-e20a65b9caa0/contacts/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"object\":\"contact\",\"contact\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284cc3f48d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:17 GMT" + }, + { + "name": "etag", + "value": "W/\"54-rfEgMCeqSJYc1agGuHEGmuCuACI\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:17.743Z", + "time": 136, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 136 + } + }, + { + "_id": "22d21e18b86b71ca50f45aee766e3595", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/82ef6091-9a99-4687-8207-e20a65b9caa0" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"82ef6091-9a99-4687-8207-e20a65b9caa0\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284d0dca8d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:18 GMT" + }, + { + "name": "etag", + "value": "W/\"50-zq6CQsEEf053K7ExinvVENpVmYY\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:18.482Z", + "time": 402, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 402 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har new file mode 100644 index 00000000..48448341 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har @@ -0,0 +1,551 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > remove > removes a contact by email", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "26223e02c7e6371faa2f0ee268a6c27c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 44, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for remove by email\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"object\":\"audience\",\"id\":\"6084b604-13ba-4d48-8412-248ab5bbe237\",\"name\":\"Test audience for remove by email\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284ad7cbfd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:12 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-2YvfE338LS5NqrbyiFmPlxmGJqc\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:12.822Z", + "time": 136, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 136 + } + }, + { + "_id": "073e179e10f6201a270d033ac04d90c7", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 44, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test-remove-by-email@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"d3c8869a-9e4d-4d9d-88f8-78ee1c813e0a\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284b21a3dd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:13 GMT" + }, + { + "name": "etag", + "value": "W/\"40-1sHkj5CP6O7fzmRQgy1H3A++5qA\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:13.563Z", + "time": 277, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 277 + } + }, + { + "_id": "bbea9c09cb3801dc4f5b2b35a5559630", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 263, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts/test-remove-by-email@example.com" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"contact\",\"contact\":\"test-remove-by-email@example.com\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284b78849d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:14 GMT" + }, + { + "name": "etag", + "value": "W/\"50-Xsi7eJcodAoVMeQ8sbTFSvP7E3I\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:14.444Z", + "time": 317, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 317 + } + }, + { + "_id": "24cb476b6b97bc1efef3d9861a406fda", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 260, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts/test-remove-by-email@example.com" + }, + "response": { + "bodySize": 67, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 67, + "text": "{\"statusCode\":404,\"message\":\"Contact not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284bd4f13d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:15 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:23:15.364Z", + "time": 119, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 119 + } + }, + { + "_id": "cb48ac2cb33bbe8039a7d42f1c65345b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"6084b604-13ba-4d48-8412-248ab5bbe237\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284c1cba7d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:16 GMT" + }, + { + "name": "etag", + "value": "W/\"50-paTxZhN68xNQaMfYLkZ45Vuz9MI\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:16.085Z", + "time": 319, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 319 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har new file mode 100644 index 00000000..bdd753e6 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har @@ -0,0 +1,551 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > remove > removes a contact by id", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "af2a077e2764b5b5f628794d35d3d720", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 41, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for remove by ID\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 105, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 105, + "text": "{\"object\":\"audience\",\"id\":\"c16d061a-2009-4517-b361-c2b86ef1600a\",\"name\":\"Test audience for remove by ID\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2849259bdd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "105" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:08 GMT" + }, + { + "name": "etag", + "value": "W/\"69-sNjp3MwpApGPS9iPF4oCewDgKG0\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 338, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:08.494Z", + "time": 138, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 138 + } + }, + { + "_id": "c27f5c20f0e03606e5c32a70adac0f9e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 41, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test-remove-by-id@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"c60f145c-d15b-4dde-a80c-c42e9938d045\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284970d66d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:09 GMT" + }, + { + "name": "etag", + "value": "W/\"40-2XMj4i9A4564FX7sT1LdpVF3kOA\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:09.236Z", + "time": 404, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 404 + } + }, + { + "_id": "6739fd118521c4510309cae2f5d4e977", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 267, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts/c60f145c-d15b-4dde-a80c-c42e9938d045" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"object\":\"contact\",\"contact\":\"c60f145c-d15b-4dde-a80c-c42e9938d045\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2849d4b89d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:10 GMT" + }, + { + "name": "etag", + "value": "W/\"54-39hTgkIWR3XiORFIu2E1oNO8DkI\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:10.244Z", + "time": 318, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 318 + } + }, + { + "_id": "51824a6c06054fe78f7edcc654458a06", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts/c60f145c-d15b-4dde-a80c-c42e9938d045" + }, + "response": { + "bodySize": 67, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 67, + "text": "{\"statusCode\":404,\"message\":\"Contact not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284a30883d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:11 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-08T03:23:11.164Z", + "time": 136, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 136 + } + }, + { + "_id": "67a911c82843a2f26a8a8388265852a0", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"c16d061a-2009-4517-b361-c2b86ef1600a\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284a7be38d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:12 GMT" + }, + { + "name": "etag", + "value": "W/\"50-5wvL7mfOj1d3krCoKv8e3OVuHaU\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:11.903Z", + "time": 308, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 308 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har new file mode 100644 index 00000000..35c995b3 --- /dev/null +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har @@ -0,0 +1,556 @@ +{ + "log": { + "_recordingName": "Contacts Integration Tests > update > updates a contact", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "67f4325e3459cfa40bc7bffaea69175a", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 35, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 182, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for update\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences" + }, + "response": { + "bodySize": 99, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 99, + "text": "{\"object\":\"audience\",\"id\":\"546f6a6a-1407-4ed6-9f9e-061ff0e9f806\",\"name\":\"Test audience for update\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b284790b2dd3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "99" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:04 GMT" + }, + { + "name": "etag", + "value": "W/\"63-p/20qfR4eBJMmRhps2Z7c7ki6Yc\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:04.436Z", + "time": 131, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 131 + } + }, + { + "_id": "80d3a0659966c921b30b53ceb2041cc2", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 35, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 228, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"email\":\"test-update@example.com\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2847d9f75d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:05 GMT" + }, + { + "name": "etag", + "value": "W/\"40-oKY1bSWE4bkaVKE1zwa9IIHPDEY\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 337, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-08T03:23:05.171Z", + "time": 374, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 374 + } + }, + { + "_id": "ed52e78f06ccb66b6d2ad8c57fef4458", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 43, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 266, + "httpVersion": "HTTP/1.1", + "method": "PATCH", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"first_name\":\"Updated\",\"last_name\":\"Name\"}" + }, + "queryString": [], + "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts/b5955a5a-9dab-4e37-8b41-19c88a570bd9" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28483bcd9d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:06 GMT" + }, + { + "name": "etag", + "value": "W/\"40-oKY1bSWE4bkaVKE1zwa9IIHPDEY\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:06.147Z", + "time": 210, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 210 + } + }, + { + "_id": "fe304c75966f38e76807b7de4db0eb7b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts/b5955a5a-9dab-4e37-8b41-19c88a570bd9" + }, + "response": { + "bodySize": 206, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 206, + "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\",\"email\":\"test-update@example.com\",\"first_name\":\"Updated\",\"last_name\":\"Name\",\"created_at\":\"2025-10-05 23:29:21.371802+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b28488c9b5d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:07 GMT" + }, + { + "name": "etag", + "value": "W/\"ce-P4qWHq31vVQIc0z4O/722nTfnyI\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "1" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:06.960Z", + "time": 125, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 125 + } + }, + { + "_id": "f694219f36b7c584d6326d4d144d0601", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.2.0-canary.2" + } + ], + "headersSize": 221, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"546f6a6a-1407-4ed6-9f9e-061ff0e9f806\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "98b2848d5d74d3b2-SEA" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 08 Oct 2025 03:23:07 GMT" + }, + { + "name": "etag", + "value": "W/\"50-k56M8zygcH9DZCxRUkWndf+66xw\"" + }, + { + "name": "ratelimit-limit", + "value": "2" + }, + { + "name": "ratelimit-policy", + "value": "2;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "0" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 367, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-08T03:23:07.687Z", + "time": 196, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 196 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/contacts/contacts.integration.spec.ts b/src/contacts/contacts.integration.spec.ts new file mode 100644 index 00000000..09ab67ef --- /dev/null +++ b/src/contacts/contacts.integration.spec.ts @@ -0,0 +1,326 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ +import type { Polly } from '@pollyjs/core'; +import { Resend } from '../resend'; +import { setupPolly } from '../test-utils/polly-setup'; + +describe('Contacts Integration Tests', () => { + let polly: Polly; + let resend: Resend; + + beforeEach(() => { + polly = setupPolly(); + resend = new Resend(process.env.RESEND_API_KEY || 're_fake_key'); + }); + + afterEach(async () => { + await polly.stop(); + }); + + describe('create', () => { + it('creates a contact', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for contact creation', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const result = await resend.contacts.create({ + email: 'test@example.com', + audienceId, + firstName: 'Test', + lastName: 'User', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.object).toBe('contact'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('handles validation errors', async () => { + // @ts-expect-error: Testing invalid input + const result = await resend.contacts.create({}); + + expect(result.error?.name).toBe('validation_error'); + }); + }); + + describe('list', () => { + it('lists contacts without pagination', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for listing', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + for (let i = 0; i < 6; i++) { + await resend.contacts.create({ + audienceId, + email: `test.${i}@example.com`, + }); + } + + const result = await resend.contacts.list({ audienceId }); + + expect(result.data?.object).toBe('list'); + expect(result.data?.data.length).toBe(6); + expect(result.data?.has_more).toBe(false); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('lists contacts with limit', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for listing with limit', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + for (let i = 0; i < 6; i++) { + await resend.contacts.create({ + audienceId, + email: `test.${i}@example.com`, + }); + } + + const result = await resend.contacts.list({ audienceId, limit: 5 }); + + expect(result.data?.data.length).toBe(5); + expect(result.data?.has_more).toBe(true); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + }); + + describe('get', () => { + it('retrieves a contact by id', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for get by ID', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const email = 'test-get-by-id@example.com'; + const createResult = await resend.contacts.create({ + email, + audienceId, + }); + + expect(createResult.data?.id).toBeTruthy(); + const contactId = createResult.data!.id; + + const getResult = await resend.contacts.get({ + id: contactId, + audienceId, + }); + + expect(getResult.data?.id).toBe(contactId); + expect(getResult.data?.email).toBe(email); + expect(getResult.data?.object).toBe('contact'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('retrieves a contact by email', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for get by email', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const email = 'test-get-by-email@example.com'; + const createResult = await resend.contacts.create({ + email, + audienceId, + }); + + expect(createResult.data?.id).toBeTruthy(); + const contactId = createResult.data!.id; + + const getResult = await resend.contacts.get({ email, audienceId }); + + expect(getResult.data?.id).toBe(contactId); + expect(getResult.data?.email).toBe(email); + expect(getResult.data?.object).toBe('contact'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('returns error for non-existent contact', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for non-existent contact', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const result = await resend.contacts.get({ + id: '00000000-0000-0000-0000-000000000000', + audienceId, + }); + + expect(result.error?.name).toBe('not_found'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + }); + + describe('update', () => { + it('updates a contact', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for update', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const createResult = await resend.contacts.create({ + email: 'test-update@example.com', + audienceId, + }); + + expect(createResult.data?.id).toBeTruthy(); + const contactId = createResult.data!.id; + + const updateResult = await resend.contacts.update({ + id: contactId, + audienceId, + firstName: 'Updated', + lastName: 'Name', + }); + + expect(updateResult.data?.id).toBe(contactId); + + const getResult = await resend.contacts.get({ + id: contactId, + audienceId, + }); + + expect(getResult.data?.first_name).toBe('Updated'); + expect(getResult.data?.last_name).toBe('Name'); + } finally { + const removeResult = await resend.audiences.remove(audienceId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + }); + + describe('remove', () => { + it('removes a contact by id', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for remove by ID', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const createResult = await resend.contacts.create({ + email: 'test-remove-by-id@example.com', + audienceId, + }); + + expect(createResult.data?.id).toBeTruthy(); + const contactId = createResult.data!.id; + + const removeResult = await resend.contacts.remove({ + id: contactId, + audienceId, + }); + + expect(removeResult.data?.deleted).toBe(true); + + const getResult = await resend.contacts.get({ + id: contactId, + audienceId, + }); + + expect(getResult.error?.name).toBe('not_found'); + } finally { + const removeAudienceResult = await resend.audiences.remove(audienceId); + expect(removeAudienceResult.data?.deleted).toBe(true); + } + }); + + it('removes a contact by email', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for remove by email', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const email = 'test-remove-by-email@example.com'; + const createResult = await resend.contacts.create({ + email, + audienceId, + }); + + expect(createResult.data?.id).toBeDefined(); + + const removeResult = await resend.contacts.remove({ + email, + audienceId, + }); + + expect(removeResult.data?.deleted).toBe(true); + + const getResult = await resend.contacts.get({ + email, + audienceId, + }); + + expect(getResult.error?.name).toBe('not_found'); + } finally { + const removeAudienceResult = await resend.audiences.remove(audienceId); + expect(removeAudienceResult.data?.deleted).toBe(true); + } + }); + + it('appears to remove a contact that never existed', async () => { + const audienceResult = await resend.audiences.create({ + name: 'Test audience for non-existent delete', + }); + + expect(audienceResult.data?.id).toBeTruthy(); + const audienceId = audienceResult.data!.id; + + try { + const result = await resend.contacts.remove({ + id: '00000000-0000-0000-0000-000000000000', + audienceId, + }); + + expect(result.data?.deleted).toBe(true); + } finally { + const removeAudienceResult = await resend.audiences.remove(audienceId); + expect(removeAudienceResult.data?.deleted).toBe(true); + } + }); + }); +}); diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts index 7ad5cf4d..ddc8325b 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -19,8 +20,12 @@ import type { } from './interfaces/remove-contact.interface'; import type { UpdateContactOptions } from './interfaces/update-contact.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + describe('Contacts', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('creates a contact', async () => { diff --git a/src/domains/domains.spec.ts b/src/domains/domains.spec.ts index ebfb3943..41ebb780 100644 --- a/src/domains/domains.spec.ts +++ b/src/domains/domains.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -12,8 +13,12 @@ import type { RemoveDomainsResponseSuccess } from './interfaces/remove-domain.in import type { UpdateDomainsResponseSuccess } from './interfaces/update-domain.interface'; import type { VerifyDomainsResponseSuccess } from './interfaces/verify-domain.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + describe('Domains', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('creates a domain', async () => { diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index eda221df..496bc618 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; @@ -8,10 +9,14 @@ import type { import type { GetEmailResponseSuccess } from './interfaces/get-email-options.interface'; import type { ListEmailsResponseSuccess } from './interfaces/list-emails-options.interface'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); describe('Emails', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('sends email', async () => { diff --git a/src/emails/receiving/receiving.spec.ts b/src/emails/receiving/receiving.spec.ts index 20c4ec42..d474e6bc 100644 --- a/src/emails/receiving/receiving.spec.ts +++ b/src/emails/receiving/receiving.spec.ts @@ -1,3 +1,4 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../../interfaces'; import { Resend } from '../../resend'; import type { @@ -5,6 +6,9 @@ import type { ListInboundEmailsResponseSuccess, } from './interfaces'; +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); describe('Receiving', () => { diff --git a/src/test-utils/polly-setup.ts b/src/test-utils/polly-setup.ts new file mode 100644 index 00000000..3712eb2c --- /dev/null +++ b/src/test-utils/polly-setup.ts @@ -0,0 +1,68 @@ +import { dirname, join } from 'node:path'; +import FetchAdapter from '@pollyjs/adapter-fetch'; +import { Polly } from '@pollyjs/core'; +import FsPersister from '@pollyjs/persister-fs'; + +Polly.register(FetchAdapter); +Polly.register(FsPersister); + +export function setupPolly() { + const { currentTestName, testPath } = expect.getState(); + if (!currentTestName || !testPath) { + throw new Error('setupPolly must be called within a test context'); + } + + const polly = new Polly(currentTestName, { + adapters: ['fetch'], + persister: 'fs', + persisterOptions: { + fs: { + recordingsDir: join(dirname(testPath), '__recordings__'), + }, + }, + mode: process.env.TEST_MODE === 'record' ? 'record' : 'replay', + recordIfMissing: process.env.TEST_MODE === 'dev', + recordFailedRequests: true, + logLevel: 'error', + matchRequestsBy: { + headers: function normalizeHeadersForMatching(headers) { + // Match all headers exactly, except authorization and user-agent, which + // should match based on presence only + const normalizedHeaders = { ...headers }; + if ('authorization' in normalizedHeaders) { + normalizedHeaders.authorization = 'present'; + } + if ('user-agent' in normalizedHeaders) { + normalizedHeaders['user-agent'] = 'present'; + } + return normalizedHeaders; + }, + }, + }); + + // Redact API keys from recordings before saving them + polly.server.any().on('beforePersist', (_, recording) => { + const resendApiKeyRegex = /re_[a-zA-Z0-9]{8}_[a-zA-Z0-9]{24}/g; + const redactApiKeys = (value: string) => + value.replace(resendApiKeyRegex, 're_REDACTED_API_KEY'); + + recording.request.headers = recording.request.headers.map( + ({ name, value }: { name: string; value: string }) => ({ + name, + value: redactApiKeys(value), + }), + ); + if (recording.request.postData?.text) { + recording.request.postData.text = redactApiKeys( + recording.request.postData.text, + ); + } + if (recording.response.content?.text) { + recording.response.content.text = redactApiKeys( + recording.response.content.text, + ); + } + }); + + return polly; +} diff --git a/vitest.config.mts b/vitest.config.mts index d0d7ba2e..c9d55dd8 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -5,5 +5,14 @@ export default defineConfig({ globals: true, environment: 'node', setupFiles: ['vitest.setup.mts'], + + /** + * When recording API responses on a rate-limited account, it's useful to + * add a timeout within `Resend.fetchRequest` and uncomment the following: + */ + // testTimeout: 30_000, + // poolOptions: { + // forks: { singleFork: true }, + // }, }, }); diff --git a/vitest.setup.mts b/vitest.setup.mts index baac05c8..d207ee98 100644 --- a/vitest.setup.mts +++ b/vitest.setup.mts @@ -1,6 +1,6 @@ -import { vi } from 'vitest'; -import createFetchMock from 'vitest-fetch-mock'; +import { config } from 'dotenv'; -const fetchMocker = createFetchMock(vi); - -fetchMocker.enableMocks(); +config({ + path: '.env.test', + quiet: true, +}); From aae3602357111ccbbe422db4e532f3c6104cbe47 Mon Sep 17 00:00:00 2001 From: Isabella Aquino Date: Tue, 14 Oct 2025 13:47:24 -0300 Subject: [PATCH 22/49] feat: bump version (#675) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65c6fd05..9142ce52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.0-canary.2", + "version": "6.2.0-canary.3", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From 653a6a380d3db82e00f91f0929da41017ace87d8 Mon Sep 17 00:00:00 2001 From: Alexandre Cisneiros Date: Wed, 15 Oct 2025 00:10:27 +0100 Subject: [PATCH 23/49] feat: merge project preview branches (#680) Co-authored-by: Carolina de Moraes Josephik <32900257+CarolinaMoraes@users.noreply.github.com> Co-authored-by: Carolina de Moraes Josephik Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Lucas da Costa Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Vitor Capretz Co-authored-by: Gabriel Miranda Co-authored-by: Vitor Capretz Co-authored-by: Cassio Zen Co-authored-by: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com> --- package.json | 3 +- src/batch/batch.spec.ts | 287 +++++ src/broadcasts/broadcasts.spec.ts | 2 + src/broadcasts/broadcasts.ts | 2 + src/broadcasts/interfaces/broadcast.ts | 1 + .../create-broadcast-options.interface.ts | 6 + .../interfaces/update-broadcast.interface.ts | 1 + .../interfaces/email-api-options.interface.ts | 9 +- src/common/interfaces/index.ts | 1 - .../pagination-options.interface.ts | 6 + .../get-pagination-query-properties.spec.ts | 38 + .../utils/get-pagination-query-properties.ts | 13 + .../utils/parse-email-to-api-options.spec.ts | 117 +- .../utils/parse-email-to-api-options.ts | 7 + .../parse-template-to-api-options.spec.ts | 349 ++++++ .../utils/parse-template-to-api-options.ts | 53 + .../recording.har | 4 +- .../audiences/contact-audiences.spec.ts | 291 +++++ src/contacts/audiences/contact-audiences.ts | 83 ++ .../add-contact-audience.interface.ts | 20 + .../interfaces/contact-audiences.interface.ts | 9 + .../list-contact-audiences.interface.ts | 22 + .../remove-contact-audience.interface.ts | 21 + src/contacts/contacts.spec.ts | 271 +++++ src/contacts/contacts.ts | 78 +- .../create-contact-options.interface.ts | 2 +- .../interfaces/get-contact.interface.ts | 8 +- .../interfaces/list-contacts.interface.ts | 7 +- .../interfaces/remove-contact.interface.ts | 8 +- .../interfaces/update-contact.interface.ts | 2 +- src/contacts/topics/contact-topics.spec.ts | 306 +++++ src/contacts/topics/contact-topics.ts | 61 + .../get-contact-topics.interface.ts | 41 + .../update-contact-topics.interface.ts | 23 + src/emails/emails.spec.ts | 195 +++- .../create-email-options.interface.ts | 30 +- .../interfaces/get-email-options.interface.ts | 1 + src/resend.ts | 4 + src/templates/chainable-template-result.ts | 39 + .../create-template-options.interface.ts | 64 + .../duplicate-template.interface.ts | 16 + .../interfaces/get-template.interface.ts | 16 + .../interfaces/list-templates.interface.ts | 33 + .../interfaces/publish-template.interface.ts | 16 + .../interfaces/remove-template.interface.ts | 16 + src/templates/interfaces/template.ts | 37 + .../interfaces/update-template.interface.ts | 52 + src/templates/templates.spec.ts | 1034 +++++++++++++++++ src/templates/templates.ts | 126 ++ .../create-topic-options.interface.ts | 15 + .../interfaces/get-contact.interface.ts | 13 + .../interfaces/list-topics.interface.ts | 11 + .../interfaces/remove-topic.interface.ts | 12 + src/topics/interfaces/topic.ts | 7 + .../interfaces/update-topic.interface.ts | 15 + src/topics/topics.spec.ts | 334 ++++++ src/topics/topics.ts | 98 ++ 57 files changed, 4312 insertions(+), 24 deletions(-) create mode 100644 src/common/utils/get-pagination-query-properties.spec.ts create mode 100644 src/common/utils/get-pagination-query-properties.ts create mode 100644 src/common/utils/parse-template-to-api-options.spec.ts create mode 100644 src/common/utils/parse-template-to-api-options.ts create mode 100644 src/contacts/audiences/contact-audiences.spec.ts create mode 100644 src/contacts/audiences/contact-audiences.ts create mode 100644 src/contacts/audiences/interfaces/add-contact-audience.interface.ts create mode 100644 src/contacts/audiences/interfaces/contact-audiences.interface.ts create mode 100644 src/contacts/audiences/interfaces/list-contact-audiences.interface.ts create mode 100644 src/contacts/audiences/interfaces/remove-contact-audience.interface.ts create mode 100644 src/contacts/topics/contact-topics.spec.ts create mode 100644 src/contacts/topics/contact-topics.ts create mode 100644 src/contacts/topics/interfaces/get-contact-topics.interface.ts create mode 100644 src/contacts/topics/interfaces/update-contact-topics.interface.ts create mode 100644 src/templates/chainable-template-result.ts create mode 100644 src/templates/interfaces/create-template-options.interface.ts create mode 100644 src/templates/interfaces/duplicate-template.interface.ts create mode 100644 src/templates/interfaces/get-template.interface.ts create mode 100644 src/templates/interfaces/list-templates.interface.ts create mode 100644 src/templates/interfaces/publish-template.interface.ts create mode 100644 src/templates/interfaces/remove-template.interface.ts create mode 100644 src/templates/interfaces/template.ts create mode 100644 src/templates/interfaces/update-template.interface.ts create mode 100644 src/templates/templates.spec.ts create mode 100644 src/templates/templates.ts create mode 100644 src/topics/interfaces/create-topic-options.interface.ts create mode 100644 src/topics/interfaces/get-contact.interface.ts create mode 100644 src/topics/interfaces/list-topics.interface.ts create mode 100644 src/topics/interfaces/remove-topic.interface.ts create mode 100644 src/topics/interfaces/topic.ts create mode 100644 src/topics/interfaces/update-topic.interface.ts create mode 100644 src/topics/topics.spec.ts create mode 100644 src/topics/topics.ts diff --git a/package.json b/package.json index 9142ce52..48f185c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.0-canary.3", + "version": "6.3.0-canary.0", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -32,6 +32,7 @@ "prepublishOnly": "pnpm run build", "test": "vitest run", "test:watch": "vitest", + "typecheck": "tsc --noEmit", "test:record": "rimraf --glob \"**/__recordings__\" && cross-env TEST_MODE=record vitest run", "test:dev": "cross-env TEST_MODE=dev vitest run" }, diff --git a/src/batch/batch.spec.ts b/src/batch/batch.spec.ts index 71cce07a..396235a1 100644 --- a/src/batch/batch.spec.ts +++ b/src/batch/batch.spec.ts @@ -370,4 +370,291 @@ describe('Batch', () => { ]); }); }); + + describe('template emails in batch', () => { + it('sends batch with template emails only', async () => { + const payload: CreateBatchOptions = [ + { + template: { + id: 'welcome-template-123', + }, + to: 'user1@example.com', + }, + { + template: { + id: 'newsletter-template-456', + variables: { + name: 'John Doe', + company: 'Acme Corp', + count: 42, + isPremium: true, + }, + }, + to: 'user2@example.com', + }, + ]; + + mockSuccessResponse( + { + data: [{ id: 'template-batch-1' }, { id: 'template-batch-2' }], + }, + { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const data = await resend.batch.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "id": "template-batch-1", + }, + { + "id": "template-batch-2", + }, + ], + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual([ + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: undefined, + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: undefined, + tags: undefined, + text: undefined, + to: 'user1@example.com', + template: { + id: 'welcome-template-123', + }, + }, + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: undefined, + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: undefined, + tags: undefined, + text: undefined, + to: 'user2@example.com', + template: { + id: 'newsletter-template-456', + variables: { + name: 'John Doe', + company: 'Acme Corp', + count: 42, + isPremium: true, + }, + }, + }, + ]); + }); + + it('sends mixed batch with template and HTML emails', async () => { + const payload: CreateBatchOptions = [ + { + from: 'sender@example.com', + to: 'user1@example.com', + subject: 'HTML Email', + html: '

Hello World

', + }, + { + template: { + id: 'welcome-template-123', + variables: { + name: 'Jane Smith', + }, + }, + to: 'user2@example.com', + }, + { + from: 'admin@example.com', + to: 'user3@example.com', + subject: 'Another HTML Email', + text: 'Plain text content', + }, + ]; + + mockSuccessResponse( + { + data: [ + { id: 'html-batch-1' }, + { id: 'template-batch-2' }, + { id: 'html-batch-3' }, + ], + }, + { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const data = await resend.batch.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "id": "html-batch-1", + }, + { + "id": "template-batch-2", + }, + { + "id": "html-batch-3", + }, + ], + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual([ + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: 'sender@example.com', + headers: undefined, + html: '

Hello World

', + reply_to: undefined, + scheduled_at: undefined, + subject: 'HTML Email', + tags: undefined, + text: undefined, + to: 'user1@example.com', + template: undefined, + }, + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: undefined, + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: undefined, + tags: undefined, + text: undefined, + to: 'user2@example.com', + template: { + id: 'welcome-template-123', + variables: { + name: 'Jane Smith', + }, + }, + }, + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: 'admin@example.com', + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: 'Another HTML Email', + tags: undefined, + text: 'Plain text content', + to: 'user3@example.com', + template: undefined, + }, + ]); + }); + + it('handles template emails with optional fields', async () => { + const payload: CreateBatchOptions = [ + { + template: { + id: 'newsletter-template-456', + variables: { + title: 'Weekly Update', + count: 150, + }, + }, + from: 'newsletter@example.com', + subject: 'Custom Subject Override', + to: 'subscriber@example.com', + replyTo: 'noreply@example.com', + scheduledAt: 'in 1 hour', + }, + ]; + + mockSuccessResponse( + { + data: [{ id: 'template-with-overrides-1' }], + }, + { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const data = await resend.batch.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "id": "template-with-overrides-1", + }, + ], + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual([ + { + attachments: undefined, + bcc: undefined, + cc: undefined, + from: 'newsletter@example.com', + headers: undefined, + html: undefined, + reply_to: 'noreply@example.com', + scheduled_at: 'in 1 hour', + subject: 'Custom Subject Override', + tags: undefined, + text: undefined, + to: 'subscriber@example.com', + template: { + id: 'newsletter-template-456', + variables: { + title: 'Weekly Update', + count: 150, + }, + }, + }, + ]); + }); + }); }); diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index 24c61204..a1f73a6e 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -436,6 +436,7 @@ describe('Broadcasts', () => { created_at: '2024-12-01T19:32:22.980Z', scheduled_at: null, sent_at: null, + topic_id: '9f31e56e-3083-46cf-8e96-c6995e0e576a', text: 'Hello world', }; @@ -468,6 +469,7 @@ describe('Broadcasts', () => { "status": "draft", "subject": "hello world", "text": "Hello world", + "topic_id": "9f31e56e-3083-46cf-8e96-c6995e0e576a", }, "error": null, } diff --git a/src/broadcasts/broadcasts.ts b/src/broadcasts/broadcasts.ts index 070d61a5..5092b7b2 100644 --- a/src/broadcasts/broadcasts.ts +++ b/src/broadcasts/broadcasts.ts @@ -51,6 +51,7 @@ export class Broadcasts { reply_to: payload.replyTo, subject: payload.subject, text: payload.text, + topic_id: payload.topicId, }, options, ); @@ -113,6 +114,7 @@ export class Broadcasts { subject: payload.subject, reply_to: payload.replyTo, preview_text: payload.previewText, + topic_id: payload.topicId, }, ); return data; diff --git a/src/broadcasts/interfaces/broadcast.ts b/src/broadcasts/interfaces/broadcast.ts index c0153441..d9a05a30 100644 --- a/src/broadcasts/interfaces/broadcast.ts +++ b/src/broadcasts/interfaces/broadcast.ts @@ -10,6 +10,7 @@ export interface Broadcast { created_at: string; scheduled_at: string | null; sent_at: string | null; + topic_id?: string | null; html: string | null; text: string | null; } diff --git a/src/broadcasts/interfaces/create-broadcast-options.interface.ts b/src/broadcasts/interfaces/create-broadcast-options.interface.ts index 69d6940b..73219884 100644 --- a/src/broadcasts/interfaces/create-broadcast-options.interface.ts +++ b/src/broadcasts/interfaces/create-broadcast-options.interface.ts @@ -61,6 +61,12 @@ interface CreateBroadcastBaseOptions { * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters */ subject: string; + /** + * The id of the topic you want to send to + * + * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters + */ + topicId?: string | null; } export type CreateBroadcastOptions = RequireAtLeastOne & diff --git a/src/broadcasts/interfaces/update-broadcast.interface.ts b/src/broadcasts/interfaces/update-broadcast.interface.ts index fa776dd3..388922ff 100644 --- a/src/broadcasts/interfaces/update-broadcast.interface.ts +++ b/src/broadcasts/interfaces/update-broadcast.interface.ts @@ -14,6 +14,7 @@ export type UpdateBroadcastOptions = { subject?: string; replyTo?: string[]; previewText?: string; + topicId?: string | null; }; export type UpdateBroadcastResponse = diff --git a/src/common/interfaces/email-api-options.interface.ts b/src/common/interfaces/email-api-options.interface.ts index 048068d1..bd6fc15b 100644 --- a/src/common/interfaces/email-api-options.interface.ts +++ b/src/common/interfaces/email-api-options.interface.ts @@ -9,9 +9,9 @@ export interface EmailApiAttachment { } export interface EmailApiOptions { - from: string; + from?: string; to: string | string[]; - subject: string; + subject?: string; region?: string; headers?: Record; html?: string; @@ -19,7 +19,12 @@ export interface EmailApiOptions { bcc?: string | string[]; cc?: string | string[]; reply_to?: string | string[]; + topic_id?: string | null; scheduled_at?: string; tags?: Tag[]; attachments?: EmailApiAttachment[]; + template?: { + id: string; + variables?: Record; + }; } diff --git a/src/common/interfaces/index.ts b/src/common/interfaces/index.ts index 11c2dd68..cb2d2e35 100644 --- a/src/common/interfaces/index.ts +++ b/src/common/interfaces/index.ts @@ -2,7 +2,6 @@ export * from './domain-api-options.interface'; export * from './email-api-options.interface'; export * from './get-option.interface'; export * from './idempotent-request.interface'; -export * from './list-option.interface'; export * from './pagination-options.interface'; export * from './patch-option.interface'; export * from './post-option.interface'; diff --git a/src/common/interfaces/pagination-options.interface.ts b/src/common/interfaces/pagination-options.interface.ts index 42136abe..4d412380 100644 --- a/src/common/interfaces/pagination-options.interface.ts +++ b/src/common/interfaces/pagination-options.interface.ts @@ -20,3 +20,9 @@ export type PaginationOptions = { after?: never; } ); + +export type PaginatedData = { + object: 'list'; + data: Data; + has_more: boolean; +}; diff --git a/src/common/utils/get-pagination-query-properties.spec.ts b/src/common/utils/get-pagination-query-properties.spec.ts new file mode 100644 index 00000000..a1b789d2 --- /dev/null +++ b/src/common/utils/get-pagination-query-properties.spec.ts @@ -0,0 +1,38 @@ +import { getPaginationQueryProperties } from './get-pagination-query-properties'; + +describe('getPaginationQueryProperties', () => { + it('returns empty string when no options provided', () => { + expect(getPaginationQueryProperties()).toBe(''); + expect(getPaginationQueryProperties({})).toBe(''); + }); + + it('builds query string with single parameter', () => { + expect(getPaginationQueryProperties({ before: 'cursor1' })).toBe( + '?before=cursor1', + ); + expect(getPaginationQueryProperties({ after: 'cursor2' })).toBe( + '?after=cursor2', + ); + expect(getPaginationQueryProperties({ limit: 10 })).toBe('?limit=10'); + }); + + it('builds query string with multiple parameters', () => { + const result = getPaginationQueryProperties({ + before: 'cursor1', + after: 'cursor2', + limit: 25, + }); + + expect(result).toBe('?before=cursor1&after=cursor2&limit=25'); + }); + + it('ignores undefined/null values', () => { + expect( + getPaginationQueryProperties({ + before: undefined, + after: 'cursor2', + limit: null, + }), + ).toBe('?after=cursor2'); + }); +}); diff --git a/src/common/utils/get-pagination-query-properties.ts b/src/common/utils/get-pagination-query-properties.ts new file mode 100644 index 00000000..37557dd7 --- /dev/null +++ b/src/common/utils/get-pagination-query-properties.ts @@ -0,0 +1,13 @@ +import type { PaginationOptions } from '../interfaces'; + +export function getPaginationQueryProperties( + options: PaginationOptions = {}, +): string { + const query = new URLSearchParams(); + + if (options.before) query.set('before', options.before); + if (options.after) query.set('after', options.after); + if (options.limit) query.set('limit', options.limit.toString()); + + return query.size > 0 ? `?${query.toString()}` : ''; +} diff --git a/src/common/utils/parse-email-to-api-options.spec.ts b/src/common/utils/parse-email-to-api-options.spec.ts index 9e1b7350..2bb8e8f3 100644 --- a/src/common/utils/parse-email-to-api-options.spec.ts +++ b/src/common/utils/parse-email-to-api-options.spec.ts @@ -2,7 +2,7 @@ import type { CreateEmailOptions } from '../../emails/interfaces/create-email-op import { parseEmailToApiOptions } from './parse-email-to-api-options'; describe('parseEmailToApiOptions', () => { - it('should handle minimal email with only required fields', () => { + it('handles minimal email with only required fields', () => { const emailPayload: CreateEmailOptions = { from: 'joao@resend.com', to: 'bu@resend.com', @@ -20,7 +20,7 @@ describe('parseEmailToApiOptions', () => { }); }); - it('should properly parse camel case to snake case', () => { + it('parses camel case to snake case', () => { const emailPayload: CreateEmailOptions = { from: 'joao@resend.com', to: 'bu@resend.com', @@ -41,4 +41,117 @@ describe('parseEmailToApiOptions', () => { scheduled_at: 'in 1 min', }); }); + + it('handles template email with template id only', () => { + const emailPayload: CreateEmailOptions = { + template: { + id: 'welcome-template-123', + }, + to: 'user@example.com', + }; + + const apiOptions = parseEmailToApiOptions(emailPayload); + + expect(apiOptions).toEqual({ + attachments: undefined, + bcc: undefined, + cc: undefined, + from: undefined, + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: undefined, + tags: undefined, + text: undefined, + to: 'user@example.com', + template: { + id: 'welcome-template-123', + }, + }); + }); + + it('handles template email with template id and variables', () => { + const emailPayload: CreateEmailOptions = { + template: { + id: 'newsletter-template-456', + variables: { + name: 'John Doe', + company: 'Acme Corp', + count: 42, + isPremium: true, + }, + }, + to: 'user@example.com', + from: 'sender@example.com', + subject: 'Custom Subject', + }; + + const apiOptions = parseEmailToApiOptions(emailPayload); + + expect(apiOptions).toEqual({ + attachments: undefined, + bcc: undefined, + cc: undefined, + from: 'sender@example.com', + headers: undefined, + html: undefined, + reply_to: undefined, + scheduled_at: undefined, + subject: 'Custom Subject', + tags: undefined, + text: undefined, + to: 'user@example.com', + template: { + id: 'newsletter-template-456', + variables: { + name: 'John Doe', + company: 'Acme Corp', + count: 42, + isPremium: true, + }, + }, + }); + }); + + it('does not include html/text fields for template emails', () => { + const emailPayload: CreateEmailOptions = { + template: { + id: 'test-template-789', + variables: { message: 'Hello World' }, + }, + to: 'user@example.com', + }; + + const apiOptions = parseEmailToApiOptions(emailPayload); + + // Verify template fields are present + expect(apiOptions.template).toEqual({ + id: 'test-template-789', + variables: { message: 'Hello World' }, + }); + + // Verify content fields are undefined + expect(apiOptions.html).toBeUndefined(); + expect(apiOptions.text).toBeUndefined(); + }); + + it('does not include template fields for content emails', () => { + const emailPayload: CreateEmailOptions = { + from: 'sender@example.com', + to: 'user@example.com', + subject: 'Test Email', + html: '

Hello World

', + text: 'Hello World', + }; + + const apiOptions = parseEmailToApiOptions(emailPayload); + + // Verify content fields are present + expect(apiOptions.html).toBe('

Hello World

'); + expect(apiOptions.text).toBe('Hello World'); + + // Verify template fields are undefined + expect(apiOptions.template).toBeUndefined(); + }); }); diff --git a/src/common/utils/parse-email-to-api-options.ts b/src/common/utils/parse-email-to-api-options.ts index e7b2ae6c..6a33535d 100644 --- a/src/common/utils/parse-email-to-api-options.ts +++ b/src/common/utils/parse-email-to-api-options.ts @@ -32,5 +32,12 @@ export function parseEmailToApiOptions( tags: email.tags, text: email.text, to: email.to, + template: email.template + ? { + id: email.template.id, + variables: email.template.variables, + } + : undefined, + topic_id: email.topicId, }; } diff --git a/src/common/utils/parse-template-to-api-options.spec.ts b/src/common/utils/parse-template-to-api-options.spec.ts new file mode 100644 index 00000000..a23a0475 --- /dev/null +++ b/src/common/utils/parse-template-to-api-options.spec.ts @@ -0,0 +1,349 @@ +import type { CreateTemplateOptions } from '../../templates/interfaces/create-template-options.interface'; +import type { UpdateTemplateOptions } from '../../templates/interfaces/update-template.interface'; +import { parseTemplateToApiOptions } from './parse-template-to-api-options'; + +describe('parseTemplateToApiOptions', () => { + it('handles minimal template with only required fields', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Welcome Template', + html: '

Welcome!

', + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions).toEqual({ + name: 'Welcome Template', + html: '

Welcome!

', + subject: undefined, + text: undefined, + alias: undefined, + from: undefined, + reply_to: undefined, + variables: undefined, + }); + }); + + it('properly converts camelCase to snake_case for all fields', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Newsletter Template', + subject: 'Weekly Newsletter', + html: '

Newsletter for {{{userName}}}!

', + text: 'Newsletter for {{{userName}}}!', + alias: 'newsletter', + from: 'newsletter@example.com', + replyTo: ['support@example.com', 'help@example.com'], + variables: [ + { + key: 'userName', + fallbackValue: 'Subscriber', + type: 'string', + }, + { + key: 'isVip', + fallbackValue: false, + type: 'boolean', + }, + ], + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions).toEqual({ + name: 'Newsletter Template', + subject: 'Weekly Newsletter', + html: '

Newsletter for {{{userName}}}!

', + text: 'Newsletter for {{{userName}}}!', + alias: 'newsletter', + from: 'newsletter@example.com', + reply_to: ['support@example.com', 'help@example.com'], + variables: [ + { + key: 'userName', + fallback_value: 'Subscriber', + type: 'string', + }, + { + key: 'isVip', + fallback_value: false, + type: 'boolean', + }, + ], + }); + }); + + it('handles single replyTo email', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Single Reply Template', + html: '

Test

', + replyTo: 'support@example.com', + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions.reply_to).toBe('support@example.com'); + }); + + it('handles update template options', () => { + const updatePayload: UpdateTemplateOptions = { + subject: 'Updated Subject', + replyTo: 'updated@example.com', + variables: [ + { + key: 'status', + fallbackValue: 'active', + type: 'string', + }, + ], + }; + + const apiOptions = parseTemplateToApiOptions(updatePayload); + + expect(apiOptions).toEqual({ + name: undefined, + subject: 'Updated Subject', + html: undefined, + text: undefined, + alias: undefined, + from: undefined, + reply_to: 'updated@example.com', + variables: [ + { + key: 'status', + fallback_value: 'active', + type: 'string', + }, + ], + }); + }); + + it('excludes React component from API options', () => { + const mockReactComponent = { + type: 'div', + props: { children: 'Hello from React!' }, + } as React.ReactElement; + + const templatePayload: CreateTemplateOptions = { + name: 'React Template', + react: mockReactComponent, + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + // React component should not be included in API options + expect(apiOptions).toEqual({ + name: 'React Template', + subject: undefined, + html: undefined, + text: undefined, + alias: undefined, + from: undefined, + reply_to: undefined, + variables: undefined, + }); + expect(apiOptions).not.toHaveProperty('react'); + }); + + it('handles variables with different types', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Multi-type Template', + html: '

Test

', + variables: [ + { + key: 'title', + fallbackValue: 'Default Title', + type: 'string', + }, + { + key: 'count', + fallbackValue: 42, + type: 'number', + }, + { + key: 'isEnabled', + fallbackValue: true, + type: 'boolean', + }, + { + key: 'optional', + fallbackValue: null, + type: 'string', + }, + ], + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions.variables).toEqual([ + { + key: 'title', + fallback_value: 'Default Title', + type: 'string', + }, + { + key: 'count', + fallback_value: 42, + type: 'number', + }, + { + key: 'isEnabled', + fallback_value: true, + type: 'boolean', + }, + { + key: 'optional', + fallback_value: null, + type: 'string', + }, + ]); + }); + + it('handles undefined variables', () => { + const templatePayload: CreateTemplateOptions = { + name: 'No Variables Template', + html: '

Simple template

', + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions.variables).toBeUndefined(); + }); + + it('handles empty variables array', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Empty Variables Template', + html: '

Template with empty variables

', + variables: [], + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions.variables).toEqual([]); + }); + + it('handles object and list variable types for create template', () => { + const templatePayload: CreateTemplateOptions = { + name: 'Complex Variables Template', + html: '

Complex template

', + variables: [ + { + key: 'userProfile', + type: 'object', + fallbackValue: { name: 'John', age: 30 }, + }, + { + key: 'tags', + type: 'list', + fallbackValue: ['premium', 'vip'], + }, + { + key: 'scores', + type: 'list', + fallbackValue: [95, 87, 92], + }, + { + key: 'flags', + type: 'list', + fallbackValue: [true, false, true], + }, + { + key: 'items', + type: 'list', + fallbackValue: [{ id: 1 }, { id: 2 }], + }, + ], + }; + + const apiOptions = parseTemplateToApiOptions(templatePayload); + + expect(apiOptions.variables).toEqual([ + { + key: 'userProfile', + type: 'object', + fallback_value: { name: 'John', age: 30 }, + }, + { + key: 'tags', + type: 'list', + fallback_value: ['premium', 'vip'], + }, + { + key: 'scores', + type: 'list', + fallback_value: [95, 87, 92], + }, + { + key: 'flags', + type: 'list', + fallback_value: [true, false, true], + }, + { + key: 'items', + type: 'list', + fallback_value: [{ id: 1 }, { id: 2 }], + }, + ]); + }); + + it('handles object and list variable types for update template', () => { + const updatePayload: UpdateTemplateOptions = { + subject: 'Updated Complex Template', + variables: [ + { + key: 'config', + type: 'object', + fallbackValue: { theme: 'dark', lang: 'en' }, + }, + { + key: 'permissions', + type: 'list', + fallbackValue: ['read', 'write'], + }, + { + key: 'counts', + type: 'list', + fallbackValue: [10, 20, 30], + }, + { + key: 'enabled', + type: 'list', + fallbackValue: [true, false], + }, + { + key: 'metadata', + type: 'list', + fallbackValue: [{ key: 'a' }, { key: 'b' }], + }, + ], + }; + + const apiOptions = parseTemplateToApiOptions(updatePayload); + + expect(apiOptions.variables).toEqual([ + { + key: 'config', + type: 'object', + fallback_value: { theme: 'dark', lang: 'en' }, + }, + { + key: 'permissions', + type: 'list', + fallback_value: ['read', 'write'], + }, + { + key: 'counts', + type: 'list', + fallback_value: [10, 20, 30], + }, + { + key: 'enabled', + type: 'list', + fallback_value: [true, false], + }, + { + key: 'metadata', + type: 'list', + fallback_value: [{ key: 'a' }, { key: 'b' }], + }, + ]); + }); +}); diff --git a/src/common/utils/parse-template-to-api-options.ts b/src/common/utils/parse-template-to-api-options.ts new file mode 100644 index 00000000..ee431e69 --- /dev/null +++ b/src/common/utils/parse-template-to-api-options.ts @@ -0,0 +1,53 @@ +import type { CreateTemplateOptions } from '../../templates/interfaces/create-template-options.interface'; +import type { TemplateVariableListFallbackType } from '../../templates/interfaces/template'; +import type { UpdateTemplateOptions } from '../../templates/interfaces/update-template.interface'; + +interface TemplateVariableApiOptions { + key: string; + type: 'string' | 'number' | 'boolean' | 'object' | 'list'; + fallback_value?: + | string + | number + | boolean + | Record + | TemplateVariableListFallbackType + | null; +} + +interface TemplateApiOptions { + name?: string; + subject?: string | null; + html?: string; + text?: string | null; + alias?: string | null; + from?: string | null; + reply_to?: string[] | string; + variables?: TemplateVariableApiOptions[]; +} + +function parseVariables( + variables: + | CreateTemplateOptions['variables'] + | UpdateTemplateOptions['variables'], +): TemplateVariableApiOptions[] | undefined { + return variables?.map((variable) => ({ + key: variable.key, + type: variable.type, + fallback_value: variable.fallbackValue, + })); +} + +export function parseTemplateToApiOptions( + template: CreateTemplateOptions | UpdateTemplateOptions, +): TemplateApiOptions { + return { + name: 'name' in template ? template.name : undefined, + subject: template.subject, + html: template.html, + text: template.text, + alias: template.alias, + from: template.from, + reply_to: template.replyTo, + variables: parseVariables(template.variables), + }; +} diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har index df1329b3..c3081e64 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "8ceccdf09452d6948d99963948117790", + "_id": "7e36d93bf833d78073dab4d08fb175ea", "_order": 0, "cache": {}, "request": { @@ -37,7 +37,7 @@ "text": "{}" }, "queryString": [], - "url": "https://api.resend.com/audiences/undefined/contacts" + "url": "https://api.resend.com/contacts" }, "response": { "bodySize": 87, diff --git a/src/contacts/audiences/contact-audiences.spec.ts b/src/contacts/audiences/contact-audiences.spec.ts new file mode 100644 index 00000000..4fbb93ef --- /dev/null +++ b/src/contacts/audiences/contact-audiences.spec.ts @@ -0,0 +1,291 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + AddContactAudiencesOptions, + AddContactAudiencesResponseSuccess, +} from './interfaces/add-contact-audience.interface'; +import type { + ListContactAudiencesOptions, + ListContactAudiencesResponseSuccess, +} from './interfaces/list-contact-audiences.interface'; +import type { + RemoveContactAudiencesOptions, + RemoveContactAudiencesResponseSuccess, +} from './interfaces/remove-contact-audience.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('ContactAudiences', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + describe('list', () => { + it('gets contact audiences by email', async () => { + const options: ListContactAudiencesOptions = { + email: 'carolina@resend.com', + }; + const response: ListContactAudiencesResponseSuccess = { + object: 'list', + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Audience', + created_at: '2021-01-01T00:00:00.000Z', + }, + { + id: 'd7e1e488-ae2c-4255-a40c-a4db3af7ed0c', + name: 'Another Audience', + created_at: '2021-01-02T00:00:00.000Z', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.list(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2021-01-01T00:00:00.000Z", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Audience", + }, + { + "created_at": "2021-01-02T00:00:00.000Z", + "id": "d7e1e488-ae2c-4255-a40c-a4db3af7ed0c", + "name": "Another Audience", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + + it('gets contact audiences by ID', async () => { + const options: ListContactAudiencesOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + limit: 1, + after: '584a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }; + const response: ListContactAudiencesResponseSuccess = { + object: 'list', + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Audience', + created_at: '2021-01-01T00:00:00.000Z', + }, + ], + has_more: true, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.list(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2021-01-01T00:00:00.000Z", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Audience", + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, +} +`); + }); + + it('returns error when missing both id and email', async () => { + const options = {}; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.audiences.list( + options as ListContactAudiencesOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('add', () => { + it('adds a contact to an audience', async () => { + const options: AddContactAudiencesOptions = { + email: 'carolina@resend.com', + audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: AddContactAudiencesResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.add(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('adds a contact to an audience by ID', async () => { + const options: AddContactAudiencesOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: AddContactAudiencesResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.add(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('returns error when missing both id and email', async () => { + const options = {}; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.audiences.add( + options as AddContactAudiencesOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('remove', () => { + it('removes a contact from an audience', async () => { + const options: RemoveContactAudiencesOptions = { + email: 'carolina@resend.com', + audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: RemoveContactAudiencesResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + deleted: true, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.remove(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('removes a contact from an audience by ID', async () => { + const options: RemoveContactAudiencesOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: RemoveContactAudiencesResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + deleted: true, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.audiences.remove(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('returns error when missing both id and email', async () => { + const options = {}; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.audiences.remove( + options as RemoveContactAudiencesOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); +}); diff --git a/src/contacts/audiences/contact-audiences.ts b/src/contacts/audiences/contact-audiences.ts new file mode 100644 index 00000000..e1254c77 --- /dev/null +++ b/src/contacts/audiences/contact-audiences.ts @@ -0,0 +1,83 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + AddContactAudiencesOptions, + AddContactAudiencesResponse, + AddContactAudiencesResponseSuccess, +} from './interfaces/add-contact-audience.interface'; +import type { + ListContactAudiencesOptions, + ListContactAudiencesResponse, + ListContactAudiencesResponseSuccess, +} from './interfaces/list-contact-audiences.interface'; +import type { + RemoveContactAudiencesOptions, + RemoveContactAudiencesResponse, + RemoveContactAudiencesResponseSuccess, +} from './interfaces/remove-contact-audience.interface'; + +export class ContactAudiences { + constructor(private readonly resend: Resend) {} + + async list( + options: ListContactAudiencesOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/contacts/${identifier}/audiences?${queryString}` + : `/contacts/${identifier}/audiences`; + + const data = + await this.resend.get(url); + return data; + } + + async add( + options: AddContactAudiencesOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + return this.resend.post( + `/contacts/${identifier}/audiences/${options.audienceId}`, + ); + } + + async remove( + options: RemoveContactAudiencesOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + return this.resend.delete( + `/contacts/${identifier}/audiences/${options.audienceId}`, + ); + } +} diff --git a/src/contacts/audiences/interfaces/add-contact-audience.interface.ts b/src/contacts/audiences/interfaces/add-contact-audience.interface.ts new file mode 100644 index 00000000..419a130b --- /dev/null +++ b/src/contacts/audiences/interfaces/add-contact-audience.interface.ts @@ -0,0 +1,20 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; + +export type AddContactAudiencesOptions = ContactAudiencesBaseOptions & { + audienceId: string; +}; + +export interface AddContactAudiencesResponseSuccess { + id: string; +} + +export type AddContactAudiencesResponse = + | { + data: AddContactAudiencesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/audiences/interfaces/contact-audiences.interface.ts b/src/contacts/audiences/interfaces/contact-audiences.interface.ts new file mode 100644 index 00000000..4ffc35cb --- /dev/null +++ b/src/contacts/audiences/interfaces/contact-audiences.interface.ts @@ -0,0 +1,9 @@ +export type ContactAudiencesBaseOptions = + | { + contactId: string; + email?: never; + } + | { + contactId?: never; + email: string; + }; diff --git a/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts b/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts new file mode 100644 index 00000000..1b0a9e2f --- /dev/null +++ b/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts @@ -0,0 +1,22 @@ +import type { Audience } from '../../../audiences/interfaces/audience'; +import type { + PaginatedData, + PaginationOptions, +} from '../../../common/interfaces/pagination-options.interface'; +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; + +export type ListContactAudiencesOptions = PaginationOptions & + ContactAudiencesBaseOptions; + +export type ListContactAudiencesResponseSuccess = PaginatedData; + +export type ListContactAudiencesResponse = + | { + data: ListContactAudiencesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts b/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts new file mode 100644 index 00000000..c6a1d322 --- /dev/null +++ b/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts @@ -0,0 +1,21 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; + +export type RemoveContactAudiencesOptions = ContactAudiencesBaseOptions & { + audienceId: string; +}; + +export interface RemoveContactAudiencesResponseSuccess { + id: string; + deleted: boolean; +} + +export type RemoveContactAudiencesResponse = + | { + data: RemoveContactAudiencesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts index ddc8325b..bef17c0d 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -143,6 +143,73 @@ describe('Contacts', () => { }), ); }); + describe('when audienceId is not provided', () => { + it('lists contacts', async () => { + const options: ListContactsOptions = { + limit: 10, + after: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + }; + + const response: ListContactsResponseSuccess = { + object: 'list', + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + email: 'team@resend.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + unsubscribed: false, + first_name: 'John', + last_name: 'Smith', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + email: 'team@react.email', + created_at: '2023-04-07T23:13:20.417116+00:00', + unsubscribed: false, + first_name: 'John', + last_name: 'Smith', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.contacts.list(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2023-04-07T23:13:52.669661+00:00", + "email": "team@resend.com", + "first_name": "John", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "last_name": "Smith", + "unsubscribed": false, + }, + { + "created_at": "2023-04-07T23:13:20.417116+00:00", + "email": "team@react.email", + "first_name": "John", + "id": "ac7503ac-e027-4aea-94b3-b0acd46f65f9", + "last_name": "Smith", + "unsubscribed": false, + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + }); }); describe('when pagination options are provided', () => { @@ -356,6 +423,181 @@ describe('Contacts', () => { } `); }); + + describe('when audienceId is not provided', () => { + it('get contact by id', async () => { + const response: GetContactResponseSuccess = { + object: 'contact', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + email: 'team@resend.com', + first_name: '', + last_name: '', + created_at: '2024-01-16T18:12:26.514Z', + unsubscribed: false, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const options: GetContactOptions = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + await expect( + resend.contacts.get(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2024-01-16T18:12:26.514Z", + "email": "team@resend.com", + "first_name": "", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "last_name": "", + "object": "contact", + "unsubscribed": false, + }, + "error": null, +} +`); + }); + + it('get contact by email', async () => { + const response: GetContactResponseSuccess = { + object: 'contact', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + email: 'team@resend.com', + first_name: '', + last_name: '', + created_at: '2024-01-16T18:12:26.514Z', + unsubscribed: false, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const options: GetContactOptions = { + email: 'team@resend.com', + }; + await expect( + resend.contacts.get(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2024-01-16T18:12:26.514Z", + "email": "team@resend.com", + "first_name": "", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "last_name": "", + "object": "contact", + "unsubscribed": false, + }, + "error": null, +} +`); + }); + it('get contact by string id', async () => { + const response: GetContactResponseSuccess = { + object: 'contact', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + email: 'team@resend.com', + first_name: '', + last_name: '', + created_at: '2024-01-16T18:12:26.514Z', + unsubscribed: false, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.get('fd61172c-cafc-40f5-b049-b45947779a29'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2024-01-16T18:12:26.514Z", + "email": "team@resend.com", + "first_name": "", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "last_name": "", + "object": "contact", + "unsubscribed": false, + }, + "error": null, +} +`); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/contacts/fd61172c-cafc-40f5-b049-b45947779a29', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('get contact by string email', async () => { + const response: GetContactResponseSuccess = { + object: 'contact', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + email: 'team@resend.com', + first_name: '', + last_name: '', + created_at: '2024-01-16T18:12:26.514Z', + unsubscribed: false, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.get('team@resend.com'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2024-01-16T18:12:26.514Z", + "email": "team@resend.com", + "first_name": "", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "last_name": "", + "object": "contact", + "unsubscribed": false, + }, + "error": null, +} +`); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/contacts/team@resend.com', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); }); describe('update', () => { @@ -459,5 +701,34 @@ describe('Contacts', () => { } `); }); + + it('removes a contact by string id', async () => { + const response: RemoveContactsResponseSuccess = { + contact: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + object: 'contact', + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.remove('3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "contact": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + "deleted": true, + "object": "contact", + }, + "error": null, +} +`); + }); }); }); diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts index fd469ca9..05cd7a24 100644 --- a/src/contacts/contacts.ts +++ b/src/contacts/contacts.ts @@ -1,5 +1,6 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; import type { Resend } from '../resend'; +import { ContactAudiences } from './audiences/contact-audiences'; import type { CreateContactOptions, CreateContactRequestOptions, @@ -12,6 +13,7 @@ import type { GetContactResponseSuccess, } from './interfaces/get-contact.interface'; import type { + ListAudienceContactsOptions, ListContactsOptions, ListContactsResponse, ListContactsResponseSuccess, @@ -26,14 +28,35 @@ import type { UpdateContactResponse, UpdateContactResponseSuccess, } from './interfaces/update-contact.interface'; +import { ContactTopics } from './topics/contact-topics'; export class Contacts { - constructor(private readonly resend: Resend) {} + readonly topics: ContactTopics; + readonly audiences: ContactAudiences; + + constructor(private readonly resend: Resend) { + this.topics = new ContactTopics(this.resend); + this.audiences = new ContactAudiences(this.resend); + } async create( payload: CreateContactOptions, options: CreateContactRequestOptions = {}, ): Promise { + if (!payload.audienceId) { + const data = await this.resend.post( + '/contacts', + { + unsubscribed: payload.unsubscribed, + email: payload.email, + first_name: payload.firstName, + last_name: payload.lastName, + }, + options, + ); + return data; + } + const data = await this.resend.post( `/audiences/${payload.audienceId}/contacts`, { @@ -47,18 +70,33 @@ export class Contacts { return data; } - async list(options: ListContactsOptions): Promise { + async list( + options: ListContactsOptions | ListAudienceContactsOptions = {}, + ): Promise { + if (!('audienceId' in options) || options.audienceId === undefined) { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/contacts?${queryString}` : '/contacts'; + const data = await this.resend.get(url); + return data; + } + const { audienceId, ...paginationOptions } = options; const queryString = buildPaginationQuery(paginationOptions); const url = queryString ? `/audiences/${audienceId}/contacts?${queryString}` : `/audiences/${audienceId}/contacts`; - const data = await this.resend.get(url); return data; } async get(options: GetContactOptions): Promise { + if (typeof options === 'string') { + const data = await this.resend.get( + `/contacts/${options}`, + ); + return data; + } + if (!options.id && !options.email) { return { data: null, @@ -70,6 +108,13 @@ export class Contacts { }; } + if (!options.audienceId) { + const data = await this.resend.get( + `/contacts/${options?.email ? options?.email : options?.id}`, + ); + return data; + } + const data = await this.resend.get( `/audiences/${options.audienceId}/contacts/${options?.email ? options?.email : options?.id}`, ); @@ -88,6 +133,18 @@ export class Contacts { }; } + if (!options.audienceId) { + const data = await this.resend.patch( + `/contacts/${options?.email ? options?.email : options?.id}`, + { + unsubscribed: options.unsubscribed, + first_name: options.firstName, + last_name: options.lastName, + }, + ); + return data; + } + const data = await this.resend.patch( `/audiences/${options.audienceId}/contacts/${options?.email ? options?.email : options?.id}`, { @@ -100,6 +157,13 @@ export class Contacts { } async remove(payload: RemoveContactOptions): Promise { + if (typeof payload === 'string') { + const data = await this.resend.delete( + `/contacts/${payload}`, + ); + return data; + } + if (!payload.id && !payload.email) { return { data: null, @@ -111,11 +175,19 @@ export class Contacts { }; } + if (!payload.audienceId) { + const data = await this.resend.delete( + `/contacts/${payload?.email ? payload?.email : payload?.id}`, + ); + return data; + } + const data = await this.resend.delete( `/audiences/${payload.audienceId}/contacts/${ payload?.email ? payload?.email : payload?.id }`, ); + return data; } } diff --git a/src/contacts/interfaces/create-contact-options.interface.ts b/src/contacts/interfaces/create-contact-options.interface.ts index ff73f25b..85402aae 100644 --- a/src/contacts/interfaces/create-contact-options.interface.ts +++ b/src/contacts/interfaces/create-contact-options.interface.ts @@ -3,7 +3,7 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; export interface CreateContactOptions { - audienceId: string; + audienceId?: string; email: string; unsubscribed?: boolean; firstName?: string; diff --git a/src/contacts/interfaces/get-contact.interface.ts b/src/contacts/interfaces/get-contact.interface.ts index 69b9f978..1e70aff6 100644 --- a/src/contacts/interfaces/get-contact.interface.ts +++ b/src/contacts/interfaces/get-contact.interface.ts @@ -1,9 +1,11 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; -export type GetContactOptions = { - audienceId: string; -} & SelectingField; +export type GetContactOptions = + | string + | ({ + audienceId?: string; + } & SelectingField); export interface GetContactResponseSuccess extends Pick< diff --git a/src/contacts/interfaces/list-contacts.interface.ts b/src/contacts/interfaces/list-contacts.interface.ts index 43fa3118..72bb9bf9 100644 --- a/src/contacts/interfaces/list-contacts.interface.ts +++ b/src/contacts/interfaces/list-contacts.interface.ts @@ -1,11 +1,16 @@ +// list-contacts.interface.ts import type { PaginationOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; -export type ListContactsOptions = { +export type ListAudienceContactsOptions = { audienceId: string; } & PaginationOptions; +export type ListContactsOptions = PaginationOptions & { + audienceId?: string; +}; + export interface ListContactsResponseSuccess { object: 'list'; data: Contact[]; diff --git a/src/contacts/interfaces/remove-contact.interface.ts b/src/contacts/interfaces/remove-contact.interface.ts index 59d2e2c0..c3e4f227 100644 --- a/src/contacts/interfaces/remove-contact.interface.ts +++ b/src/contacts/interfaces/remove-contact.interface.ts @@ -7,9 +7,11 @@ export type RemoveContactsResponseSuccess = { contact: string; }; -export type RemoveContactOptions = SelectingField & { - audienceId: string; -}; +export type RemoveContactOptions = + | string + | (SelectingField & { + audienceId?: string; + }); export type RemoveContactsResponse = | { diff --git a/src/contacts/interfaces/update-contact.interface.ts b/src/contacts/interfaces/update-contact.interface.ts index c5a60ff5..49831c7d 100644 --- a/src/contacts/interfaces/update-contact.interface.ts +++ b/src/contacts/interfaces/update-contact.interface.ts @@ -2,7 +2,7 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; export type UpdateContactOptions = { - audienceId: string; + audienceId?: string; unsubscribed?: boolean; firstName?: string; lastName?: string; diff --git a/src/contacts/topics/contact-topics.spec.ts b/src/contacts/topics/contact-topics.spec.ts new file mode 100644 index 00000000..93fdcba7 --- /dev/null +++ b/src/contacts/topics/contact-topics.spec.ts @@ -0,0 +1,306 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + GetContactTopicsOptions, + GetContactTopicsResponseSuccess, +} from './interfaces/get-contact-topics.interface'; +import type { + UpdateContactTopicsOptions, + UpdateContactTopicsResponseSuccess, +} from './interfaces/update-contact-topics.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('ContactTopics', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('update', () => { + it('updates contact topics with opt_in', async () => { + const payload: UpdateContactTopicsOptions = { + email: 'carolina+2@resend.com', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + subscription: 'opt_in', + }, + ], + }; + const response: UpdateContactTopicsResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('updates contact topics with opt_out', async () => { + const payload: UpdateContactTopicsOptions = { + email: 'carolina+2@resend.com', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + subscription: 'opt_out', + }, + ], + }; + const response: UpdateContactTopicsResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('updates contact topics with both opt_in and opt_out', async () => { + const payload: UpdateContactTopicsOptions = { + email: 'carolina+2@resend.com', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + subscription: 'opt_in', + }, + { + id: 'another-topic-id', + subscription: 'opt_out', + }, + ], + }; + const response: UpdateContactTopicsResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('returns error when missing both id and email', async () => { + const payload = { + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + subscription: 'opt_in', + }, + ], + }; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.topics.update( + payload as UpdateContactTopicsOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + + it('updates contact topics using ID', async () => { + const payload: UpdateContactTopicsOptions = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + subscription: 'opt_in', + }, + ], + }; + const response: UpdateContactTopicsResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + }); + + describe('get', () => { + it('gets contact topics by email', async () => { + const options: GetContactTopicsOptions = { + email: 'carolina@resend.com', + }; + const response: GetContactTopicsResponseSuccess = { + has_more: false, + object: 'list', + data: { + email: 'carolina@resend.com', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Topic', + description: 'This is a test topic', + subscription: 'opt_in', + }, + { + id: 'another-topic-id', + name: 'Another Topic', + description: null, + subscription: 'opt_out', + }, + ], + }, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.get(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": { + "email": "carolina@resend.com", + "topics": [ + { + "description": "This is a test topic", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Topic", + "subscription": "opt_in", + }, + { + "description": null, + "id": "another-topic-id", + "name": "Another Topic", + "subscription": "opt_out", + }, + ], + }, + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + + it('gets contact topics by ID', async () => { + const options: GetContactTopicsOptions = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + const response: GetContactTopicsResponseSuccess = { + has_more: false, + object: 'list', + data: { + email: 'carolina@resend.com', + topics: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Topic', + description: 'This is a test topic', + subscription: 'opt_in', + }, + ], + }, + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.topics.get(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": { + "email": "carolina@resend.com", + "topics": [ + { + "description": "This is a test topic", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Topic", + "subscription": "opt_in", + }, + ], + }, + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + + it('returns error when missing both id and email', async () => { + const options = {}; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.topics.get( + options as GetContactTopicsOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); +}); diff --git a/src/contacts/topics/contact-topics.ts b/src/contacts/topics/contact-topics.ts new file mode 100644 index 00000000..1eb15b0c --- /dev/null +++ b/src/contacts/topics/contact-topics.ts @@ -0,0 +1,61 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + GetContactTopicsOptions, + GetContactTopicsResponse, + GetContactTopicsResponseSuccess, +} from './interfaces/get-contact-topics.interface'; +import type { + UpdateContactTopicsOptions, + UpdateContactTopicsResponse, + UpdateContactTopicsResponseSuccess, +} from './interfaces/update-contact-topics.interface'; + +export class ContactTopics { + constructor(private readonly resend: Resend) {} + + async update( + payload: UpdateContactTopicsOptions, + ): Promise { + if (!payload.id && !payload.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + name: 'missing_required_field', + }, + }; + } + + const identifier = payload.email ? payload.email : payload.id; + const data = await this.resend.patch( + `/contacts/${identifier}/topics`, + payload.topics, + ); + + return data; + } + + async get( + options: GetContactTopicsOptions, + ): Promise { + if (!options.id && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.id; + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/contacts/${identifier}/topics?${queryString}` + : `/contacts/${identifier}/topics`; + + const data = await this.resend.get(url); + return data; + } +} diff --git a/src/contacts/topics/interfaces/get-contact-topics.interface.ts b/src/contacts/topics/interfaces/get-contact-topics.interface.ts new file mode 100644 index 00000000..5a0c4e29 --- /dev/null +++ b/src/contacts/topics/interfaces/get-contact-topics.interface.ts @@ -0,0 +1,41 @@ +import type { GetOptions } from '../../../common/interfaces'; +import type { + PaginatedData, + PaginationOptions, +} from '../../../common/interfaces/pagination-options.interface'; +import type { ErrorResponse } from '../../../interfaces'; + +interface GetContactTopicsBaseOptions { + id?: string; + email?: string; +} + +export type GetContactTopicsOptions = GetContactTopicsBaseOptions & + PaginationOptions; + +export interface GetContactTopicsRequestOptions extends GetOptions {} + +export interface ContactTopic { + id: string; + name: string; + description: string | null; + subscription: 'opt_in' | 'opt_out'; +} + +export type GetContactTopicsResponseSuccess = PaginatedData<{ + email: string; + topics: ContactTopic[]; +}>; + +export type GetContactTopicsResponse = + | { + data: PaginatedData<{ + email: string; + topics: ContactTopic[]; + }>; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/topics/interfaces/update-contact-topics.interface.ts b/src/contacts/topics/interfaces/update-contact-topics.interface.ts new file mode 100644 index 00000000..739c268b --- /dev/null +++ b/src/contacts/topics/interfaces/update-contact-topics.interface.ts @@ -0,0 +1,23 @@ +import type { PatchOptions } from '../../../common/interfaces/patch-option.interface'; +import type { ErrorResponse } from '../../../interfaces'; + +interface UpdateContactTopicsBaseOptions { + id?: string; + email?: string; +} + +export interface UpdateContactTopicsOptions + extends UpdateContactTopicsBaseOptions { + topics: { id: string; subscription: 'opt_in' | 'opt_out' }[]; +} + +export interface UpdateContactTopicsRequestOptions extends PatchOptions {} + +export interface UpdateContactTopicsResponseSuccess { + id: string; +} + +export interface UpdateContactTopicsResponse { + data: UpdateContactTopicsResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index 496bc618..f0eed3ec 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -66,6 +66,7 @@ describe('Emails', () => { to: 'user@resend.com', subject: 'Not Idempotent Test', html: '

Test

', + topicId: '9f31e56e-3083-46cf-8e96-c6995e0e576a', }; await resend.emails.create(payload); @@ -76,8 +77,18 @@ describe('Emails', () => { const request = lastCall[1]; expect(request).toBeDefined(); - const headers = new Headers(request?.headers); - expect(headers.has('Idempotency-Key')).toBe(false); + // Make sure the topic_id is included in the body + expect(lastCall[1]?.body).toEqual( + '{"from":"admin@resend.com","html":"

Test

","subject":"Not Idempotent Test","to":"user@resend.com","topic_id":"9f31e56e-3083-46cf-8e96-c6995e0e576a"}', + ); + + //@ts-expect-error + const hasIdempotencyKey = lastCall[1]?.headers.has('Idempotency-Key'); + expect(hasIdempotencyKey).toBeFalsy(); + + //@ts-expect-error + const usedIdempotencyKey = lastCall[1]?.headers.get('Idempotency-Key'); + expect(usedIdempotencyKey).toBeNull(); }); it('sends the Idempotency-Key header when idempotencyKey is provided', async () => { @@ -396,6 +407,186 @@ describe('Emails', () => { }), ); }); + + describe('template emails', () => { + it('sends email with template id only', async () => { + const response: CreateEmailResponseSuccess = { + id: 'template-email-123', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const payload: CreateEmailOptions = { + template: { + id: 'welcome-template-123', + }, + to: 'user@example.com', + }; + + const data = await resend.emails.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "id": "template-email-123", + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual({ + template: { + id: 'welcome-template-123', + }, + to: 'user@example.com', + }); + }); + + it('sends email with template id and variables', async () => { + const response: CreateEmailResponseSuccess = { + id: 'template-vars-email-456', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const payload: CreateEmailOptions = { + template: { + id: 'welcome-template-123', + variables: { + name: 'John Doe', + company: 'Acme Corp', + welcomeBonus: 100, + isPremium: true, + }, + }, + to: 'user@example.com', + }; + + const data = await resend.emails.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "id": "template-vars-email-456", + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual({ + template: { + id: 'welcome-template-123', + variables: { + name: 'John Doe', + company: 'Acme Corp', + welcomeBonus: 100, + isPremium: true, + }, + }, + to: 'user@example.com', + }); + }); + + it('sends template email with optional from and subject', async () => { + const response: CreateEmailResponseSuccess = { + id: 'template-with-overrides-789', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const payload: CreateEmailOptions = { + template: { + id: 'welcome-template-123', + variables: { + name: 'Jane Smith', + }, + }, + from: 'custom@example.com', + subject: 'Custom Subject Override', + to: 'user@example.com', + }; + + const data = await resend.emails.send(payload); + expect(data).toMatchInlineSnapshot(` +{ + "data": { + "id": "template-with-overrides-789", + }, + "error": null, +} +`); + + // Verify the correct API payload was sent + const lastCall = fetchMock.mock.calls[0]; + const requestBody = JSON.parse(lastCall[1]?.body as string); + expect(requestBody).toEqual({ + template: { + id: 'welcome-template-123', + variables: { + name: 'Jane Smith', + }, + }, + from: 'custom@example.com', + subject: 'Custom Subject Override', + to: 'user@example.com', + }); + }); + + it('handles template email errors correctly', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const payload: CreateEmailOptions = { + template: { + id: 'invalid-template-123', + }, + to: 'user@example.com', + }; + + const result = await resend.emails.send(payload); + expect(result).toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, +} +`); + }); + }); }); describe('get', () => { diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index 11db678a..5aa9afe0 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -25,6 +25,19 @@ interface EmailRenderOptions { text: string; } +interface EmailTemplateOptions { + template: { + id: string; + variables?: Record; + }; +} + +interface CreateEmailBaseOptionsWithTemplate + extends Omit { + from?: string; + subject?: string; +} + interface CreateEmailBaseOptions { /** * Filename and content of attachments (max 40mb per email) @@ -80,6 +93,12 @@ interface CreateEmailBaseOptions { * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters */ to: string | string[]; + /** + * The id of the topic you want to send to + * + * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters + */ + topicId?: string | null; /** * Schedule email to be sent later. * The date should be in ISO 8601 format (e.g: 2024-08-05T11:52:01.858Z). @@ -89,8 +108,15 @@ interface CreateEmailBaseOptions { scheduledAt?: string; } -export type CreateEmailOptions = RequireAtLeastOne & - CreateEmailBaseOptions; +export type CreateEmailOptions = + | ((RequireAtLeastOne & CreateEmailBaseOptions) & { + template?: never; + }) + | ((EmailTemplateOptions & CreateEmailBaseOptionsWithTemplate) & { + react?: never; + html?: never; + text?: never; + }); export interface CreateEmailRequestOptions extends PostOptions, diff --git a/src/emails/interfaces/get-email-options.interface.ts b/src/emails/interfaces/get-email-options.interface.ts index 1d83248a..ac1b5b9e 100644 --- a/src/emails/interfaces/get-email-options.interface.ts +++ b/src/emails/interfaces/get-email-options.interface.ts @@ -24,6 +24,7 @@ export interface GetEmailResponseSuccess { text: string | null; tags?: { name: string; value: string }[]; to: string[]; + topic_id?: string | null; scheduled_at: string | null; object: 'email'; } diff --git a/src/resend.ts b/src/resend.ts index 9c6f97ef..2d2eacd1 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -11,6 +11,8 @@ import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; import type { ErrorResponse } from './interfaces'; +import { Templates } from './templates/templates'; +import { Topics } from './topics/topics'; import { Webhooks } from './webhooks/webhooks'; const defaultBaseUrl = 'https://api.resend.com'; @@ -36,6 +38,8 @@ export class Resend { readonly domains = new Domains(this); readonly emails = new Emails(this); readonly webhooks = new Webhooks(); + readonly templates = new Templates(this); + readonly topics = new Topics(this); constructor(readonly key?: string) { if (!key) { diff --git a/src/templates/chainable-template-result.ts b/src/templates/chainable-template-result.ts new file mode 100644 index 00000000..786937e1 --- /dev/null +++ b/src/templates/chainable-template-result.ts @@ -0,0 +1,39 @@ +import type { CreateTemplateResponse } from './interfaces/create-template-options.interface'; +import type { DuplicateTemplateResponse } from './interfaces/duplicate-template.interface'; +import type { PublishTemplateResponse } from './interfaces/publish-template.interface'; + +export class ChainableTemplateResult< + T extends CreateTemplateResponse | DuplicateTemplateResponse, +> implements PromiseLike +{ + constructor( + private readonly promise: Promise, + private readonly publishFn: ( + id: string, + ) => Promise, + ) {} + + // If user calls `then` or only awaits for the result of create() or duplicate(), the behavior should be + // exactly as if they called create() or duplicate() directly. This will act as a normal promise + + // biome-ignore lint/suspicious/noThenProperty: This class intentionally implements PromiseLike + then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): PromiseLike { + return this.promise.then(onfulfilled, onrejected); + } + + async publish(): Promise { + const { data, error } = await this.promise; + + if (error) { + return { + data: null, + error, + }; + } + const publishResult = await this.publishFn(data.id); + return publishResult; + } +} diff --git a/src/templates/interfaces/create-template-options.interface.ts b/src/templates/interfaces/create-template-options.interface.ts new file mode 100644 index 00000000..92548b28 --- /dev/null +++ b/src/templates/interfaces/create-template-options.interface.ts @@ -0,0 +1,64 @@ +import type { PostOptions } from '../../common/interfaces'; +import type { RequireAtLeastOne } from '../../common/interfaces/require-at-least-one'; +import type { ErrorResponse } from '../../interfaces'; +import type { + Template, + TemplateVariable, + TemplateVariableListFallbackType, +} from './template'; + +type TemplateContentCreationOptions = RequireAtLeastOne<{ + html: string; + react: React.ReactNode; +}>; + +type TemplateVariableCreationOptions = Pick & + ( + | { + type: 'string'; + fallbackValue?: string | null; + } + | { + type: 'number'; + fallbackValue?: number | null; + } + | { + type: 'boolean'; + fallbackValue?: boolean | null; + } + | { + type: 'object'; + fallbackValue: Record; + } + | { + type: 'list'; + fallbackValue: TemplateVariableListFallbackType; + } + ); + +type TemplateOptionalFieldsForCreation = Partial< + Pick +> & { + replyTo?: string[] | string; + variables?: TemplateVariableCreationOptions[]; +}; + +export type CreateTemplateOptions = Pick & + TemplateOptionalFieldsForCreation & + TemplateContentCreationOptions; + +export interface CreateTemplateRequestOptions extends PostOptions {} + +export interface CreateTemplateResponseSuccess extends Pick { + object: 'template'; +} + +export type CreateTemplateResponse = + | { + data: CreateTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/duplicate-template.interface.ts b/src/templates/interfaces/duplicate-template.interface.ts new file mode 100644 index 00000000..eb685843 --- /dev/null +++ b/src/templates/interfaces/duplicate-template.interface.ts @@ -0,0 +1,16 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Template } from './template'; + +export interface DuplicateTemplateResponseSuccess extends Pick { + object: 'template'; +} + +export type DuplicateTemplateResponse = + | { + data: DuplicateTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/get-template.interface.ts b/src/templates/interfaces/get-template.interface.ts new file mode 100644 index 00000000..f9724d27 --- /dev/null +++ b/src/templates/interfaces/get-template.interface.ts @@ -0,0 +1,16 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Template } from './template'; + +export interface GetTemplateResponseSuccess extends Template { + object: 'template'; +} + +export type GetTemplateResponse = + | { + data: GetTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/list-templates.interface.ts b/src/templates/interfaces/list-templates.interface.ts new file mode 100644 index 00000000..27522345 --- /dev/null +++ b/src/templates/interfaces/list-templates.interface.ts @@ -0,0 +1,33 @@ +import type { PaginationOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; +import type { Template } from './template'; + +export type ListTemplatesOptions = PaginationOptions; + +interface TemplateListItem + extends Pick< + Template, + | 'id' + | 'name' + | 'created_at' + | 'updated_at' + | 'status' + | 'published_at' + | 'alias' + > {} + +export interface ListTemplatesResponseSuccess { + object: 'list'; + data: TemplateListItem[]; + has_more: boolean; +} + +export type ListTemplatesResponse = + | { + data: ListTemplatesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/publish-template.interface.ts b/src/templates/interfaces/publish-template.interface.ts new file mode 100644 index 00000000..5cd7a4e4 --- /dev/null +++ b/src/templates/interfaces/publish-template.interface.ts @@ -0,0 +1,16 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Template } from './template'; + +export interface PublishTemplateResponseSuccess extends Pick { + object: 'template'; +} + +export type PublishTemplateResponse = + | { + data: PublishTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/remove-template.interface.ts b/src/templates/interfaces/remove-template.interface.ts new file mode 100644 index 00000000..f1823a4b --- /dev/null +++ b/src/templates/interfaces/remove-template.interface.ts @@ -0,0 +1,16 @@ +import type { ErrorResponse } from '../../interfaces'; + +export interface RemoveTemplateResponseSuccess { + object: 'template'; + id: string; + deleted: boolean; +} +export type RemoveTemplateResponse = + | { + data: RemoveTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/interfaces/template.ts b/src/templates/interfaces/template.ts new file mode 100644 index 00000000..94dc740b --- /dev/null +++ b/src/templates/interfaces/template.ts @@ -0,0 +1,37 @@ +export interface Template { + id: string; + name: string; + subject: string | null; + html: string; + text: string | null; + status: 'draft' | 'published'; + variables: TemplateVariable[] | null; + alias: string | null; + from: string | null; + reply_to: string[] | null; + published_at: string | null; + created_at: string; + updated_at: string; + has_unpublished_versions: boolean; + current_version_id: string; +} + +export type TemplateVariableListFallbackType = + | string[] + | number[] + | boolean[] + | Record[]; + +export interface TemplateVariable { + key: string; + fallback_value: + | string + | number + | boolean + | Record + | TemplateVariableListFallbackType + | null; + type: 'string' | 'number' | 'boolean' | 'object' | 'list'; + created_at: string; + updated_at: string; +} diff --git a/src/templates/interfaces/update-template.interface.ts b/src/templates/interfaces/update-template.interface.ts new file mode 100644 index 00000000..f1b5275e --- /dev/null +++ b/src/templates/interfaces/update-template.interface.ts @@ -0,0 +1,52 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { + Template, + TemplateVariable, + TemplateVariableListFallbackType, +} from './template'; + +type TemplateVariableUpdateOptions = Pick & + ( + | { + type: 'string'; + fallbackValue?: string | null; + } + | { + type: 'number'; + fallbackValue?: number | null; + } + | { + type: 'boolean'; + fallbackValue?: boolean | null; + } + | { + type: 'object'; + fallbackValue: Record; + } + | { + type: 'list'; + fallbackValue: TemplateVariableListFallbackType; + } + ); + +export interface UpdateTemplateOptions + extends Partial< + Pick + > { + variables?: TemplateVariableUpdateOptions[]; + replyTo?: string[] | string; +} + +export interface UpdateTemplateResponseSuccess extends Pick { + object: 'template'; +} + +export type UpdateTemplateResponse = + | { + data: UpdateTemplateResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/templates/templates.spec.ts b/src/templates/templates.spec.ts new file mode 100644 index 00000000..9aacfd5d --- /dev/null +++ b/src/templates/templates.spec.ts @@ -0,0 +1,1034 @@ +import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + CreateTemplateOptions, + CreateTemplateResponseSuccess, +} from './interfaces/create-template-options.interface'; +import type { GetTemplateResponseSuccess } from './interfaces/get-template.interface'; +import type { UpdateTemplateOptions } from './interfaces/update-template.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const mockRenderAsync = vi.fn(); +vi.mock('@react-email/render', () => ({ + renderAsync: mockRenderAsync, +})); + +const TEST_API_KEY = 're_test_api_key'; +describe('Templates', () => { + afterEach(() => { + vi.resetAllMocks(); + fetchMock.resetMocks(); + }); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a template with minimal required fields', async () => { + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + html: '

Welcome to our platform!

', + }; + const response: CreateTemplateResponseSuccess = { + object: 'template', + id: '3deaccfb-f47f-440a-8875-ea14b1716b43', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + await expect( + resend.templates.create(payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "3deaccfb-f47f-440a-8875-ea14b1716b43", + "object": "template", + }, + "error": null, + } + `); + }); + + it('creates a template with all optional fields', async () => { + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + subject: 'Welcome to our platform', + html: '

Welcome to our platform, {{{name}}}!

We are excited to have you join {{{company}}}.

', + text: 'Welcome to our platform, {{{name}}}! We are excited to have you join {{{company}}}.', + variables: [ + { + key: 'name', + fallbackValue: 'User', + type: 'string', + }, + { + key: 'company', + fallbackValue: 'Company', + type: 'string', + }, + ], + alias: 'welcome-email', + from: 'noreply@example.com', + replyTo: ['support@example.com', 'help@example.com'], + }; + const response: CreateTemplateResponseSuccess = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + await expect( + resend.templates.create(payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + + it('throws error when template validation fails', async () => { + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + html: '

Welcome {{{user_email}}}!

', // Uses undefined variable + variables: [ + { + key: 'user_name', + type: 'string', + fallbackValue: 'Guest', + }, + ], + }; + const response: ErrorResponse = { + name: 'validation_error', + message: + "Variable 'user_email' is used in the template but not defined in the variables list", + }; + + mockErrorResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + const result = resend.templates.create(payload); + + await expect(result).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Variable 'user_email' is used in the template but not defined in the variables list", + "name": "validation_error", + }, + } + `); + }); + + it('creates template with React component', async () => { + const mockReactComponent = { + type: 'div', + props: { children: 'Welcome!' }, + } as React.ReactElement; + + mockRenderAsync.mockResolvedValueOnce('
Welcome!
'); + + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + react: mockReactComponent, + }; + + const response: CreateTemplateResponseSuccess = { + object: 'template', + id: '3deaccfb-f47f-440a-8875-ea14b1716b43', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + await expect( + resend.templates.create(payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "3deaccfb-f47f-440a-8875-ea14b1716b43", + "object": "template", + }, + "error": null, + } + `); + + expect(mockRenderAsync).toHaveBeenCalledWith(mockReactComponent); + }); + + it('creates template with React component and all optional fields', async () => { + const mockReactComponent = { + type: 'div', + props: { + children: [ + { type: 'h1', props: { children: 'Welcome {name}!' } }, + { type: 'p', props: { children: 'Welcome to {company}.' } }, + ], + }, + } as React.ReactElement; + + mockRenderAsync.mockResolvedValueOnce( + '

Welcome {{{name}}}!

Welcome to {{{company}}}.

', + ); + + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + subject: 'Welcome to our platform', + react: mockReactComponent, + text: 'Welcome {{{name}}}! Welcome to {{{company}}}.', + variables: [ + { + key: 'name', + fallbackValue: 'User', + type: 'string', + }, + { + key: 'company', + fallbackValue: 'Company', + type: 'string', + }, + ], + alias: 'welcome-email', + from: 'noreply@example.com', + replyTo: ['support@example.com', 'help@example.com'], + }; + + const response: CreateTemplateResponseSuccess = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + await expect( + resend.templates.create(payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + + expect(mockRenderAsync).toHaveBeenCalledWith(mockReactComponent); + }); + + it('throws error when React renderer fails to load', async () => { + const mockReactComponent = { + type: 'div', + props: { children: 'Welcome!' }, + } as React.ReactElement; + + // Temporarily clear the mock implementation to simulate module load failure + mockRenderAsync.mockImplementationOnce(() => { + throw new Error( + 'Failed to render React component. Make sure to install `@react-email/render`', + ); + }); + + const payload: CreateTemplateOptions = { + name: 'Welcome Email', + react: mockReactComponent, + }; + + const resend = new Resend(TEST_API_KEY); + + await expect(resend.templates.create(payload)).rejects.toThrow( + 'Failed to render React component. Make sure to install `@react-email/render`', + ); + }); + + it('creates a template with object and list variable types', async () => { + const payload: CreateTemplateOptions = { + name: 'Complex Variables Template', + html: '

Welcome {{{userProfile.name}}}!

Your tags: {{{tags}}}

', + variables: [ + { + key: 'userProfile', + type: 'object', + fallbackValue: { name: 'John', age: 30 }, + }, + { + key: 'tags', + type: 'list', + fallbackValue: ['premium', 'vip'], + }, + { + key: 'scores', + type: 'list', + fallbackValue: [95, 87, 92], + }, + { + key: 'flags', + type: 'list', + fallbackValue: [true, false, true], + }, + { + key: 'items', + type: 'list', + fallbackValue: [{ id: 1 }, { id: 2 }], + }, + ], + }; + const response: CreateTemplateResponseSuccess = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + await expect( + resend.templates.create(payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + }); + + describe('remove', () => { + it('removes a template', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response = { + object: 'template', + id, + deleted: true, + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect(resend.templates.remove(id)).resolves.toMatchInlineSnapshot(` + { + "data": { + "deleted": true, + "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", + "object": "template", + }, + "error": null, + } + `); + }); + + it('throws error when template not found', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + mockErrorResponse(response, { + status: 404, + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect(resend.templates.remove(id)).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, + } + `); + }); + }); + + describe('duplicate', () => { + it('duplicates a template', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.duplicate(id), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + + it('throws error when template not found', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + mockErrorResponse(response, { + status: 404, + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.duplicate(id), + ).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, + } + `); + }); + }); + + describe('update', () => { + it('updates a template with minimal fields', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const payload: UpdateTemplateOptions = { + name: 'Updated Welcome Email', + }; + const response = { + object: 'template', + id, + }; + + mockSuccessResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.templates.update(id, payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", + "object": "template", + }, + "error": null, + } + `); + }); + + it('updates a template with all optional fields', async () => { + const id = 'fd61172c-cafc-40f5-b049-b45947779a29'; + const payload: UpdateTemplateOptions = { + name: 'Updated Welcome Email', + subject: 'Updated Welcome to our platform', + html: '

Updated Welcome to our platform, {{{name}}}!

We are excited to have you join {{{company}}}.

', + text: 'Updated Welcome to our platform, {{{name}}}! We are excited to have you join {{{company}}}.', + variables: [ + { + key: 'name', + fallbackValue: 'User', + type: 'string', + }, + { + key: 'company', + type: 'string', + fallbackValue: 'User', + }, + ], + alias: 'updated-welcome-email', + from: 'updated@example.com', + replyTo: ['updated-support@example.com'], + }; + const response = { + object: 'template', + id, + }; + + mockSuccessResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.templates.update(id, payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + + it('throws error when template not found', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const payload: UpdateTemplateOptions = { + name: 'Updated Welcome Email', + }; + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + mockErrorResponse(response, { + status: 404, + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.templates.update(id, payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, + } + `); + }); + + it('updates a template with object and list variable types', async () => { + const id = 'fd61172c-cafc-40f5-b049-b45947779a29'; + const payload: UpdateTemplateOptions = { + name: 'Updated Complex Variables Template', + html: '

Updated Welcome {{{config.theme}}}!

Permissions: {{{permissions}}}

', + variables: [ + { + key: 'config', + type: 'object', + fallbackValue: { theme: 'dark', lang: 'en' }, + }, + { + key: 'permissions', + type: 'list', + fallbackValue: ['read', 'write'], + }, + { + key: 'counts', + type: 'list', + fallbackValue: [10, 20, 30], + }, + { + key: 'enabled', + type: 'list', + fallbackValue: [true, false], + }, + { + key: 'metadata', + type: 'list', + fallbackValue: [{ key: 'a' }, { key: 'b' }], + }, + ], + }; + const response = { + object: 'template', + id, + }; + + mockSuccessResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.templates.update(id, payload), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + }); + + describe('get', () => { + describe('when template not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + mockErrorResponse(response, { + status: 404, + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.get('non-existent-id'), + ).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, + } + `); + }); + }); + + it('get template', async () => { + const response: GetTemplateResponseSuccess = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'Welcome Email', + created_at: '2025-08-19 19:28:27.947052+00', + updated_at: '2025-08-19 19:28:27.947052+00', + html: '

Welcome!

', + text: 'Welcome!', + subject: 'Welcome to our platform', + status: 'published', + alias: 'welcome-email', + from: 'noreply@example.com', + reply_to: ['support@example.com'], + published_at: '2025-08-19 19:28:27.947052+00', + has_unpublished_versions: false, + current_version_id: 'ver_123456', + variables: [ + { + key: 'name', + type: 'string', + fallback_value: 'User', + created_at: '2025-08-19 19:28:27.947052+00', + updated_at: '2025-08-19 19:28:27.947052+00', + }, + ], + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.get('fd61172c-cafc-40f5-b049-b45947779a29'), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "alias": "welcome-email", + "created_at": "2025-08-19 19:28:27.947052+00", + "current_version_id": "ver_123456", + "from": "noreply@example.com", + "has_unpublished_versions": false, + "html": "

Welcome!

", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "Welcome Email", + "object": "template", + "published_at": "2025-08-19 19:28:27.947052+00", + "reply_to": [ + "support@example.com", + ], + "status": "published", + "subject": "Welcome to our platform", + "text": "Welcome!", + "updated_at": "2025-08-19 19:28:27.947052+00", + "variables": [ + { + "created_at": "2025-08-19 19:28:27.947052+00", + "fallback_value": "User", + "key": "name", + "type": "string", + "updated_at": "2025-08-19 19:28:27.947052+00", + }, + ], + }, + "error": null, + } + `); + }); + }); + + describe('publish', () => { + it('publishes a template', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response = { + object: 'template', + id, + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.publish(id), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", + "object": "template", + }, + "error": null, + } + `); + }); + + it('throws error when template not found', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response: ErrorResponse = { + name: 'not_found', + message: 'Template not found', + }; + + mockErrorResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.publish(id), + ).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Template not found", + "name": "not_found", + }, + } + `); + }); + + describe('chaining with create', () => { + it('chains create().publish() successfully', async () => { + const createResponse = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + const publishResponse = { + object: 'template', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + }; + + // Mock create request + fetchMock.mockOnceIf( + (req) => + req.url.includes('/templates') && !req.url.includes('publish'), + JSON.stringify(createResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${TEST_API_KEY}`, + }, + }, + ); + + // Mock publish request + fetchMock.mockOnceIf( + (req) => + req.url.includes('/templates') && req.url.includes('publish'), + JSON.stringify(publishResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${TEST_API_KEY}`, + }, + }, + ); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates + .create({ + name: 'Welcome Email', + html: '

Welcome!

', + }) + .publish(), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "object": "template", + }, + "error": null, + } + `); + }); + }); + + describe('chaining with duplicate', () => { + it('chains duplicate().publish() successfully', async () => { + const duplicateResponse = { + object: 'template', + id: 'new-template-id-123', + }; + + const publishResponse = { + object: 'template', + id: 'new-template-id-123', + }; + + // Mock duplicate request + fetchMock.mockOnceIf( + (req) => req.url.includes('/duplicate'), + JSON.stringify(duplicateResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${TEST_API_KEY}`, + }, + }, + ); + + // Mock publish request + fetchMock.mockOnceIf( + (req) => req.url.includes('/publish'), + JSON.stringify(publishResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${TEST_API_KEY}`, + }, + }, + ); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.duplicate('original-template-id').publish(), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "id": "new-template-id-123", + "object": "template", + }, + "error": null, + } + `); + }); + }); + }); + + describe('list', () => { + it('lists templates without pagination options', async () => { + const response = { + object: 'list', + has_more: false, + data: [ + { + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'Welcome Email', + created_at: '2023-04-07T23:13:52.669661+00:00', + updated_at: '2023-04-07T23:13:52.669661+00:00', + status: 'published', + alias: 'welcome-email', + published_at: '2023-04-07T23:13:52.669661+00:00', + }, + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + name: 'Newsletter Template', + created_at: '2023-04-06T20:10:30.417116+00:00', + updated_at: '2023-04-06T20:10:30.417116+00:00', + status: 'draft', + alias: 'newsletter', + published_at: null, + }, + ], + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect(resend.templates.list()).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "alias": "welcome-email", + "created_at": "2023-04-07T23:13:52.669661+00:00", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "Welcome Email", + "published_at": "2023-04-07T23:13:52.669661+00:00", + "status": "published", + "updated_at": "2023-04-07T23:13:52.669661+00:00", + }, + { + "alias": "newsletter", + "created_at": "2023-04-06T20:10:30.417116+00:00", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Newsletter Template", + "published_at": null, + "status": "draft", + "updated_at": "2023-04-06T20:10:30.417116+00:00", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + } + `); + + // Verify the request was made without query parameters + expect(fetchMock).toHaveBeenCalledWith( + expect.stringMatching(/^https?:\/\/[^/]+\/templates$/), + expect.objectContaining({ + method: 'GET', + }), + ); + }); + + it('lists templates with pagination options', async () => { + const response = { + object: 'list', + has_more: true, + data: [ + { + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'Welcome Email', + created_at: '2023-04-07T23:13:52.669661+00:00', + updated_at: '2023-04-07T23:13:52.669661+00:00', + status: 'published', + alias: 'welcome-email', + published_at: '2023-04-07T23:13:52.669661+00:00', + }, + ], + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await expect( + resend.templates.list({ + before: 'cursor123', + limit: 10, + }), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "alias": "welcome-email", + "created_at": "2023-04-07T23:13:52.669661+00:00", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "Welcome Email", + "published_at": "2023-04-07T23:13:52.669661+00:00", + "status": "published", + "updated_at": "2023-04-07T23:13:52.669661+00:00", + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, + } + `); + + // Verify the request was made with correct query parameters + const [url] = fetchMock.mock.calls[0]; + const parsedUrl = new URL(url as string); + + expect(parsedUrl.pathname).toBe('/templates'); + expect(parsedUrl.searchParams.get('before')).toBe('cursor123'); + expect(parsedUrl.searchParams.get('limit')).toBe('10'); + }); + + it('handles all pagination options', async () => { + const response = { + object: 'list', + has_more: false, + data: [], + }; + + mockSuccessResponse(response, { + headers: { Authorization: `Bearer ${TEST_API_KEY}` }, + }); + + const resend = new Resend(TEST_API_KEY); + + await resend.templates.list({ + before: 'cursor1', + after: 'cursor2', + limit: 25, + }); + + // Verify all pagination parameters are included + const [url] = fetchMock.mock.calls[0]; + const parsedUrl = new URL(url as string); + + expect(parsedUrl.pathname).toBe('/templates'); + expect(parsedUrl.searchParams.get('before')).toBe('cursor1'); + expect(parsedUrl.searchParams.get('after')).toBe('cursor2'); + expect(parsedUrl.searchParams.get('limit')).toBe('25'); + }); + }); +}); diff --git a/src/templates/templates.ts b/src/templates/templates.ts new file mode 100644 index 00000000..85551e66 --- /dev/null +++ b/src/templates/templates.ts @@ -0,0 +1,126 @@ +import type { PaginationOptions } from '../common/interfaces'; +import { getPaginationQueryProperties } from '../common/utils/get-pagination-query-properties'; +import { parseTemplateToApiOptions } from '../common/utils/parse-template-to-api-options'; +import type { Resend } from '../resend'; +import { ChainableTemplateResult } from './chainable-template-result'; +import type { + CreateTemplateOptions, + CreateTemplateResponse, + CreateTemplateResponseSuccess, +} from './interfaces/create-template-options.interface'; +import type { + DuplicateTemplateResponse, + DuplicateTemplateResponseSuccess, +} from './interfaces/duplicate-template.interface'; +import type { + GetTemplateResponse, + GetTemplateResponseSuccess, +} from './interfaces/get-template.interface'; +import type { + ListTemplatesResponse, + ListTemplatesResponseSuccess, +} from './interfaces/list-templates.interface'; +import type { + PublishTemplateResponse, + PublishTemplateResponseSuccess, +} from './interfaces/publish-template.interface'; +import type { + RemoveTemplateResponse, + RemoveTemplateResponseSuccess, +} from './interfaces/remove-template.interface'; +import type { + UpdateTemplateOptions, + UpdateTemplateResponse, + UpdateTemplateResponseSuccess, +} from './interfaces/update-template.interface'; + +export class Templates { + private renderAsync?: (component: React.ReactElement) => Promise; + constructor(private readonly resend: Resend) {} + + create( + payload: CreateTemplateOptions, + ): ChainableTemplateResult { + const createPromise = this.performCreate(payload); + return new ChainableTemplateResult(createPromise, this.publish.bind(this)); + } + // This creation process is being done separately from the public create so that + // the user can chain the publish operation after the create operation. Otherwise, due + // to the async nature of the renderAsync, the return type would be + // Promise> which wouldn't be chainable. + private async performCreate( + payload: CreateTemplateOptions, + ): Promise { + if (payload.react) { + if (!this.renderAsync) { + try { + const { renderAsync } = await import('@react-email/render'); + this.renderAsync = renderAsync; + } catch { + throw new Error( + 'Failed to render React component. Make sure to install `@react-email/render`', + ); + } + } + + payload.html = await this.renderAsync( + payload.react as React.ReactElement, + ); + } + + return this.resend.post( + '/templates', + parseTemplateToApiOptions(payload), + ); + } + + async remove(identifier: string): Promise { + const data = await this.resend.delete( + `/templates/${identifier}`, + ); + return data; + } + + async get(identifier: string): Promise { + const data = await this.resend.get( + `/templates/${identifier}`, + ); + return data; + } + + async list(options: PaginationOptions = {}): Promise { + return this.resend.get( + `/templates${getPaginationQueryProperties(options)}`, + ); + } + + duplicate( + identifier: string, + ): ChainableTemplateResult { + const promiseDuplicate = this.resend.post( + `/templates/${identifier}/duplicate`, + ); + return new ChainableTemplateResult( + promiseDuplicate, + this.publish.bind(this), + ); + } + + async publish(identifier: string): Promise { + const data = await this.resend.post( + `/templates/${identifier}/publish`, + ); + return data; + } + + async update( + identifier: string, + payload: UpdateTemplateOptions, + ): Promise { + const data = await this.resend.patch( + `/templates/${identifier}`, + parseTemplateToApiOptions(payload), + ); + return data; + } +} diff --git a/src/topics/interfaces/create-topic-options.interface.ts b/src/topics/interfaces/create-topic-options.interface.ts new file mode 100644 index 00000000..41100306 --- /dev/null +++ b/src/topics/interfaces/create-topic-options.interface.ts @@ -0,0 +1,15 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Topic } from './topic'; + +export interface CreateTopicOptions { + name: string; + description?: string; + defaultSubscription: 'opt_in' | 'opt_out'; +} + +export type CreateTopicResponseSuccess = Pick; + +export interface CreateTopicResponse { + data: CreateTopicResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/topics/interfaces/get-contact.interface.ts b/src/topics/interfaces/get-contact.interface.ts new file mode 100644 index 00000000..f3de6ac8 --- /dev/null +++ b/src/topics/interfaces/get-contact.interface.ts @@ -0,0 +1,13 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Topic } from './topic'; + +export interface GetTopicOptions { + id: string; +} + +export type GetTopicResponseSuccess = Topic; + +export interface GetTopicResponse { + data: GetTopicResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/topics/interfaces/list-topics.interface.ts b/src/topics/interfaces/list-topics.interface.ts new file mode 100644 index 00000000..e90aa6ea --- /dev/null +++ b/src/topics/interfaces/list-topics.interface.ts @@ -0,0 +1,11 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Topic } from './topic'; + +export interface ListTopicsResponseSuccess { + data: Topic[]; +} + +export interface ListTopicsResponse { + data: ListTopicsResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/topics/interfaces/remove-topic.interface.ts b/src/topics/interfaces/remove-topic.interface.ts new file mode 100644 index 00000000..2d80584e --- /dev/null +++ b/src/topics/interfaces/remove-topic.interface.ts @@ -0,0 +1,12 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Topic } from './topic'; + +export type RemoveTopicResponseSuccess = Pick & { + object: 'topic'; + deleted: boolean; +}; + +export interface RemoveTopicResponse { + data: RemoveTopicResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/topics/interfaces/topic.ts b/src/topics/interfaces/topic.ts new file mode 100644 index 00000000..dc7a2d7e --- /dev/null +++ b/src/topics/interfaces/topic.ts @@ -0,0 +1,7 @@ +export interface Topic { + id: string; + name: string; + description?: string; + defaultSubscription: 'opt_in' | 'opt_out'; + created_at: string; +} diff --git a/src/topics/interfaces/update-topic.interface.ts b/src/topics/interfaces/update-topic.interface.ts new file mode 100644 index 00000000..f78f2fee --- /dev/null +++ b/src/topics/interfaces/update-topic.interface.ts @@ -0,0 +1,15 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Topic } from './topic'; + +export interface UpdateTopicOptions { + id: string; + name?: string; + description?: string; +} + +export type UpdateTopicResponseSuccess = Pick; + +export interface UpdateTopicResponse { + data: UpdateTopicResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/topics/topics.spec.ts b/src/topics/topics.spec.ts new file mode 100644 index 00000000..b5978be8 --- /dev/null +++ b/src/topics/topics.spec.ts @@ -0,0 +1,334 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + CreateTopicOptions, + CreateTopicResponseSuccess, +} from './interfaces/create-topic-options.interface'; +import type { GetTopicResponseSuccess } from './interfaces/get-contact.interface'; +import type { ListTopicsResponseSuccess } from './interfaces/list-topics.interface'; +import type { RemoveTopicResponseSuccess } from './interfaces/remove-topic.interface'; +import type { UpdateTopicOptions } from './interfaces/update-topic.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('Topics', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a topic', async () => { + const payload: CreateTopicOptions = { + name: 'Newsletter', + description: 'Weekly newsletter updates', + defaultSubscription: 'opt_in', + }; + const response: CreateTopicResponseSuccess = { + id: '3deaccfb-f47f-440a-8875-ea14b1716b43', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.topics.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3deaccfb-f47f-440a-8875-ea14b1716b43", + }, + "error": null, +} +`); + }); + + it('throws error when missing name', async () => { + const payload: CreateTopicOptions = { + name: '', + defaultSubscription: 'opt_in', + }; + const response: ErrorResponse = { + name: 'missing_required_field', + message: 'Missing `name` field.', + }; + + mockErrorResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.topics.create(payload); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`name\` field.", + "name": "missing_required_field", + }, +} +`); + }); + + it('throws error when missing defaultSubscription', async () => { + const payload = { + name: 'Newsletter', + description: 'Weekly newsletter updates', + }; + const response: ErrorResponse = { + name: 'missing_required_field', + message: 'Missing `defaultSubscription` field.', + }; + + mockErrorResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.topics.create(payload as CreateTopicOptions); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`defaultSubscription\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('list', () => { + it('lists topics', async () => { + const response: ListTopicsResponseSuccess = { + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + name: 'Newsletter', + description: 'Weekly newsletter updates', + defaultSubscription: 'opt_in', + created_at: '2023-04-07T23:13:52.669661+00:00', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + name: 'Product Updates', + description: 'Product announcements and updates', + defaultSubscription: 'opt_out', + created_at: '2023-04-07T23:13:20.417116+00:00', + }, + ], + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.topics.list()).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2023-04-07T23:13:52.669661+00:00", + "defaultSubscription": "opt_in", + "description": "Weekly newsletter updates", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Newsletter", + }, + { + "created_at": "2023-04-07T23:13:20.417116+00:00", + "defaultSubscription": "opt_out", + "description": "Product announcements and updates", + "id": "ac7503ac-e027-4aea-94b3-b0acd46f65f9", + "name": "Product Updates", + }, + ], + }, + "error": null, +} +`); + }); + }); + + describe('get', () => { + describe('when topic not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Topic not found', + }; + + mockErrorResponse(response, { + headers: { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.topics.get( + '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Topic not found", + "name": "not_found", + }, +} +`); + }); + }); + + it('get topic by id', async () => { + const response: GetTopicResponseSuccess = { + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'Newsletter', + description: 'Weekly newsletter updates', + defaultSubscription: 'opt_in', + created_at: '2024-01-16T18:12:26.514Z', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.topics.get('fd61172c-cafc-40f5-b049-b45947779a29'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2024-01-16T18:12:26.514Z", + "defaultSubscription": "opt_in", + "description": "Weekly newsletter updates", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "Newsletter", + }, + "error": null, +} +`); + }); + + it('returns error when missing id', async () => { + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = resend.topics.get(''); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('update', () => { + it('updates a topic', async () => { + const payload: UpdateTopicOptions = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + name: 'Updated Newsletter', + description: 'Updated weekly newsletter', + }; + const response = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.topics.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + }, + "error": null, +} +`); + }); + + it('returns error when missing id', async () => { + const payload = { + name: 'Updated Newsletter', + }; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.topics.update(payload as UpdateTopicOptions); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('remove', () => { + it('removes a topic', async () => { + const response: RemoveTopicResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + object: 'topic', + deleted: true, + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.topics.remove('3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + "object": "topic", + }, + "error": null, +} +`); + }); + + it('returns error when missing id', async () => { + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = resend.topics.remove(''); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); +}); diff --git a/src/topics/topics.ts b/src/topics/topics.ts new file mode 100644 index 00000000..f46d9c1c --- /dev/null +++ b/src/topics/topics.ts @@ -0,0 +1,98 @@ +import type { Resend } from '../resend'; +import type { + CreateTopicOptions, + CreateTopicResponse, + CreateTopicResponseSuccess, +} from './interfaces/create-topic-options.interface'; +import type { + GetTopicResponse, + GetTopicResponseSuccess, +} from './interfaces/get-contact.interface'; +import type { + ListTopicsResponse, + ListTopicsResponseSuccess, +} from './interfaces/list-topics.interface'; +import type { + RemoveTopicResponse, + RemoveTopicResponseSuccess, +} from './interfaces/remove-topic.interface'; +import type { + UpdateTopicOptions, + UpdateTopicResponse, + UpdateTopicResponseSuccess, +} from './interfaces/update-topic.interface'; + +export class Topics { + constructor(private readonly resend: Resend) {} + + async create(payload: CreateTopicOptions): Promise { + const { defaultSubscription, ...body } = payload; + + const data = await this.resend.post('/topics', { + ...body, + defaultSubscription: defaultSubscription, + }); + + return data; + } + + async list(): Promise { + const data = await this.resend.get('/topics'); + + return data; + } + + async get(id: string): Promise { + if (!id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + name: 'missing_required_field', + }, + }; + } + const data = await this.resend.get( + `/topics/${id}`, + ); + + return data; + } + + async update(payload: UpdateTopicOptions): Promise { + if (!payload.id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + name: 'missing_required_field', + }, + }; + } + + const data = await this.resend.patch( + `/topics/${payload.id}`, + payload, + ); + + return data; + } + + async remove(id: string): Promise { + if (!id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + name: 'missing_required_field', + }, + }; + } + + const data = await this.resend.delete( + `/topics/${id}`, + ); + + return data; + } +} From 27a9a6c6b48ac3e6a1c95ae88d8597d21503fb08 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Wed, 15 Oct 2025 11:40:05 -0300 Subject: [PATCH 24/49] feat: download from API's signed URLs instead of proxied routes (#676) --- .../receiving/interfaces/attachment.ts | 10 + .../interfaces/get-attachment.interface.ts | 13 +- .../interfaces/list-attachments.interface.ts | 14 + src/attachments/receiving/receiving.spec.ts | 432 ++++++++++++++---- src/attachments/receiving/receiving.ts | 113 ++++- 5 files changed, 474 insertions(+), 108 deletions(-) diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts index 718868a6..0f003ad1 100644 --- a/src/attachments/receiving/interfaces/attachment.ts +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -6,3 +6,13 @@ export interface InboundAttachment { content_id?: string; content: string; // base64 } + +export interface ApiInboundAttachment { + id: string; + filename?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; + download_url: string; + expires_at: string; +} diff --git a/src/attachments/receiving/interfaces/get-attachment.interface.ts b/src/attachments/receiving/interfaces/get-attachment.interface.ts index 74901024..7bb34439 100644 --- a/src/attachments/receiving/interfaces/get-attachment.interface.ts +++ b/src/attachments/receiving/interfaces/get-attachment.interface.ts @@ -1,25 +1,16 @@ import type { ErrorResponse } from '../../../interfaces'; -import type { InboundAttachment } from './attachment'; +import type { ApiInboundAttachment, InboundAttachment } from './attachment'; export interface GetAttachmentOptions { emailId: string; id: string; } -// API response type (snake_case from API) export interface GetAttachmentApiResponse { object: 'attachment'; - data: { - id: string; - filename?: string; - content_type: string; - content_disposition: 'inline' | 'attachment'; - content_id?: string; - content: string; - }; + data: ApiInboundAttachment; } -// SDK response type (camelCase for users) export interface GetAttachmentResponseSuccess { object: 'attachment'; data: InboundAttachment; diff --git a/src/attachments/receiving/interfaces/list-attachments.interface.ts b/src/attachments/receiving/interfaces/list-attachments.interface.ts index 7d5dfdbd..3f174645 100644 --- a/src/attachments/receiving/interfaces/list-attachments.interface.ts +++ b/src/attachments/receiving/interfaces/list-attachments.interface.ts @@ -6,6 +6,20 @@ export type ListAttachmentsOptions = PaginationOptions & { emailId: string; }; +export interface ListAttachmentsApiResponse { + object: 'list'; + has_more: boolean; + data: Array<{ + id: string; + filename?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; + download_url: string; + expires_at: string; + }>; +} + export interface ListAttachmentsResponseSuccess { object: 'list'; has_more: boolean; diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts index d42c2b45..e25fc905 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -2,14 +2,28 @@ import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../../interfaces'; import { Resend } from '../../resend'; import { mockSuccessResponse } from '../../test-utils/mock-fetch'; -import type { GetAttachmentResponseSuccess } from './interfaces'; -import type { ListAttachmentsResponseSuccess } from './interfaces/list-attachments.interface'; +import type { + ListAttachmentsApiResponse, + ListAttachmentsResponseSuccess, +} from './interfaces/list-attachments.interface'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); +const pdfContent = 'PDF document content'; +const pdfBuffer = Buffer.from(pdfContent); +const pdfBase64 = pdfBuffer.toString('base64'); + +const imageContent = 'PNG image data'; +const imageBuffer = Buffer.from(imageContent); +const imageBase64 = imageBuffer.toString('base64'); + +const textContent = 'Plain text content'; +const textBuffer = Buffer.from(textContent); +const textBase64 = textBuffer.toString('base64'); + describe('Receiving', () => { afterEach(() => fetchMock.resetMocks()); afterAll(() => fetchMocker.disableMocks()); @@ -49,7 +63,7 @@ describe('Receiving', () => { describe('when attachment found', () => { it('returns attachment with transformed fields', async () => { - const apiResponse: GetAttachmentResponseSuccess = { + const apiResponse = { object: 'attachment' as const, data: { id: 'att_123', @@ -57,39 +71,49 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment' as const, - content: 'base64encodedcontent==', + download_url: 'https://example.com/download/att_123', }, }; - fetchMock.mockOnce(JSON.stringify(apiResponse), { - status: 200, - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_123', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, }, - }); + ); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_123', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": { - "data": { - "content": "base64encodedcontent==", - "content_disposition": "attachment", - "content_id": "cid_123", - "content_type": "application/pdf", - "filename": "document.pdf", - "id": "att_123", - }, - "object": "attachment", - }, - "error": null, -} -`); + expect(result).toEqual({ + data: { + data: { + content: pdfBase64, + content_disposition: 'attachment', + content_id: 'cid_123', + content_type: 'application/pdf', + filename: 'document.pdf', + id: 'att_123', + }, + object: 'attachment', + }, + error: null, + }); }); it('returns inline attachment', async () => { @@ -101,40 +125,49 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline' as const, - content: - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', + download_url: 'https://example.com/download/att_456', }, }; - fetchMock.mockOnce(JSON.stringify(apiResponse), { - status: 200, - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_456', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, }, - }); + ); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_456', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": { - "data": { - "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", - "content_disposition": "inline", - "content_id": "cid_456", - "content_type": "image/png", - "filename": "image.png", - "id": "att_456", - }, - "object": "attachment", - }, - "error": null, -} -`); + expect(result).toEqual({ + data: { + data: { + content: imageBase64, + content_disposition: 'inline', + content_id: 'cid_456', + content_type: 'image/png', + filename: 'image.png', + id: 'att_456', + }, + object: 'attachment', + }, + error: null, + }); }); it('handles attachment without optional fields (filename, contentId)', async () => { @@ -145,44 +178,54 @@ describe('Receiving', () => { id: 'att_789', content_type: 'text/plain', content_disposition: 'attachment' as const, - content: 'base64content', + download_url: 'https://example.com/download/att_789', // Optional fields (filename, content_id) omitted }, }; - fetchMock.mockOnce(JSON.stringify(apiResponse), { - status: 200, - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_789', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, }, - }); + ); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_789', + async () => + new Response(textBuffer, { + status: 200, + }), + ); const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_789', }); - expect(result).toMatchInlineSnapshot(` -{ - "data": { - "data": { - "content": "base64content", - "content_disposition": "attachment", - "content_type": "text/plain", - "id": "att_789", - }, - "object": "attachment", - }, - "error": null, -} -`); + expect(result).toEqual({ + data: { + data: { + content: textBase64, + content_disposition: 'attachment', + content_type: 'text/plain', + id: 'att_789', + }, + object: 'attachment', + }, + error: null, + }); }); }); }); describe('list', () => { - const apiResponse: ListAttachmentsResponseSuccess = { + const apiResponse: ListAttachmentsApiResponse = { object: 'list' as const, has_more: false, data: [ @@ -192,7 +235,7 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment' as const, - content: 'base64encodedcontent==', + download_url: 'https://example.com/download/att_123', }, { id: 'att_456', @@ -200,7 +243,7 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline' as const, - content: 'imagebase64==', + download_url: 'https://example.com/download/att_456', }, ], }; @@ -234,34 +277,81 @@ describe('Receiving', () => { describe('when attachments found', () => { it('returns multiple attachments', async () => { - fetchMock.mockOnce(JSON.stringify(apiResponse), { - status: 200, - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, }, - }); + ); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); - expect(result).toEqual({ data: apiResponse, error: null }); + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + content: pdfBase64, + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + content: imageBase64, + }, + ], + }; + + expect(result).toEqual({ data: expectedResponse, error: null }); }); it('returns empty array when no attachments', async () => { const emptyResponse = { - object: 'attachment' as const, + object: 'list' as const, + has_more: false, data: [], }; - fetchMock.mockOnce(JSON.stringify(emptyResponse), { - status: 200, - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + JSON.stringify(emptyResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, }, - }); + ); const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', @@ -277,12 +367,50 @@ describe('Receiving', () => { headers, }); + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); + const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + content: pdfBase64, + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + content: imageBase64, + }, + ], + }; + expect(result).toEqual({ - data: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -294,12 +422,52 @@ describe('Receiving', () => { describe('when pagination options are provided', () => { it('calls endpoint passing limit param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); + const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', limit: 10, }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + content: pdfBase64, + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + content: imageBase64, + }, + ], + }; + expect(result).toEqual({ - data: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -309,12 +477,52 @@ describe('Receiving', () => { it('calls endpoint passing after param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); + const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', after: 'cursor123', }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + content: pdfBase64, + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + content: imageBase64, + }, + ], + }; + expect(result).toEqual({ - data: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -324,12 +532,52 @@ describe('Receiving', () => { it('calls endpoint passing before param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); + + fetchMock.mockOnceIf( + 'https://example.com/download/att_123', + async () => + new Response(pdfBuffer, { + status: 200, + }), + ); + fetchMock.mockOnceIf( + 'https://example.com/download/att_456', + async () => + new Response(imageBuffer, { + status: 200, + }), + ); + const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', before: 'cursor123', }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + content: pdfBase64, + }, + { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + content: imageBase64, + }, + ], + }; + expect(result).toEqual({ - data: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( diff --git a/src/attachments/receiving/receiving.ts b/src/attachments/receiving/receiving.ts index f13748f8..439a54db 100644 --- a/src/attachments/receiving/receiving.ts +++ b/src/attachments/receiving/receiving.ts @@ -1,27 +1,95 @@ import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { + GetAttachmentApiResponse, GetAttachmentOptions, GetAttachmentResponse, GetAttachmentResponseSuccess, } from './interfaces/get-attachment.interface'; import type { + ListAttachmentsApiResponse, ListAttachmentsOptions, ListAttachmentsResponse, - ListAttachmentsResponseSuccess, } from './interfaces/list-attachments.interface'; +type DownloadAttachmentResult = + | { + type: 'error'; + message: string; + } + | { + type: 'success'; + base64Content: string; + }; + export class Receiving { constructor(private readonly resend: Resend) {} + private async downloadAttachment( + url: string, + ): Promise { + try { + const content = await fetch(url); + if (!content.ok) { + return { + type: 'error', + message: 'Failed to download attachment content', + }; + } + + const arrayBuffer = await content.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + return { + type: 'success', + base64Content: buffer.toString('base64'), + }; + } catch { + return { + type: 'error', + message: 'An error occurred while downloading attachment content', + }; + } + } + async get(options: GetAttachmentOptions): Promise { const { emailId, id } = options; - const data = await this.resend.get( + const apiResponse = await this.resend.get( `/emails/receiving/${emailId}/attachments/${id}`, ); - return data; + if ('error' in apiResponse && apiResponse.error) { + return apiResponse; + } + + const { + expires_at: _expires_at, + download_url, + ...otherFields + } = apiResponse.data.data; + const downloadResult = await this.downloadAttachment(download_url); + if (downloadResult.type === 'error') { + return { + data: null, + error: { + name: 'application_error', + message: downloadResult.message, + }, + }; + } + + const responseData: GetAttachmentResponseSuccess = { + object: 'attachment', + data: { + ...otherFields, + content: downloadResult.base64Content, + }, + }; + + return { + data: responseData, + error: null, + }; } async list( @@ -34,8 +102,43 @@ export class Receiving { ? `/emails/receiving/${emailId}/attachments?${queryString}` : `/emails/receiving/${emailId}/attachments`; - const data = await this.resend.get(url); + const apiResponse = await this.resend.get(url); + + if ('error' in apiResponse && apiResponse.error) { + return apiResponse; + } + + const attachmentsWithContent = []; + for (const attachment of apiResponse.data.data) { + const { + expires_at: _expires_at, + download_url, + ...otherFields + } = attachment; + const downloadResult = await this.downloadAttachment(download_url); + if (downloadResult.type === 'error') { + return { + data: null, + error: { + name: 'application_error', + message: downloadResult.message, + }, + }; + } + + attachmentsWithContent.push({ + ...otherFields, + content: downloadResult.base64Content, + }); + } - return data; + return { + data: { + object: 'list', + has_more: apiResponse.data.has_more, + data: attachmentsWithContent, + }, + error: null, + }; } } From 4ba83407c02290250ce6c50bbdde9101d45f6ee8 Mon Sep 17 00:00:00 2001 From: Zeno Rocha Date: Wed, 15 Oct 2025 08:26:21 -0700 Subject: [PATCH 25/49] fix: repo urls (#679) --- package.json | 6 +++--- readme.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 48f185c1..ddb5b962 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/resendlabs/resend-node.git" + "url": "git+https://github.com/resend/resend-node.git" }, "author": "", "license": "MIT", "bugs": { - "url": "https://github.com/resendlabs/resend-node/issues" + "url": "https://github.com/resend/resend-node/issues" }, - "homepage": "https://github.com/resendlabs/resend-node#readme", + "homepage": "https://github.com/resend/resend-node#readme", "dependencies": { "svix": "1.76.1" }, diff --git a/readme.md b/readme.md index 1a6288b3..557b4179 100644 --- a/readme.md +++ b/readme.md @@ -36,10 +36,10 @@ yarn add resend Send email with: -- [Node.js](https://github.com/resendlabs/resend-node-example) -- [Next.js (App Router)](https://github.com/resendlabs/resend-nextjs-app-router-example) -- [Next.js (Pages Router)](https://github.com/resendlabs/resend-nextjs-pages-router-example) -- [Express](https://github.com/resendlabs/resend-express-example) +- [Node.js](https://github.com/resend/resend-node-example) +- [Next.js (App Router)](https://github.com/resend/resend-nextjs-app-router-example) +- [Next.js (Pages Router)](https://github.com/resend/resend-nextjs-pages-router-example) +- [Express](https://github.com/resend/resend-express-example) ## Setup From afc815a30c74fa888a40e2b31c2d6070f5d477e1 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Wed, 15 Oct 2025 12:27:49 -0300 Subject: [PATCH 26/49] feat: allow creating domain with capabilities through the api (#682) --- .../domain-api-options.interface.ts | 1 + .../utils/parse-domain-to-api-options.ts | 1 + src/domains/domains.spec.ts | 46 +++++++++++++++++++ .../create-domain-options.interface.ts | 6 ++- src/domains/interfaces/domain.ts | 16 ++++++- .../interfaces/get-domain.interface.ts | 5 +- 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/common/interfaces/domain-api-options.interface.ts b/src/common/interfaces/domain-api-options.interface.ts index 5d99efe8..9e615fa2 100644 --- a/src/common/interfaces/domain-api-options.interface.ts +++ b/src/common/interfaces/domain-api-options.interface.ts @@ -2,4 +2,5 @@ export interface DomainApiOptions { name: string; region?: string; custom_return_path?: string; + capability?: 'send' | 'receive' | 'send-and-receive'; } diff --git a/src/common/utils/parse-domain-to-api-options.ts b/src/common/utils/parse-domain-to-api-options.ts index cfaa31c1..0c20f961 100644 --- a/src/common/utils/parse-domain-to-api-options.ts +++ b/src/common/utils/parse-domain-to-api-options.ts @@ -7,6 +7,7 @@ export function parseDomainToApiOptions( return { name: domain.name, region: domain.region, + capability: domain.capability, custom_return_path: domain.customReturnPath, }; } diff --git a/src/domains/domains.spec.ts b/src/domains/domains.spec.ts index 41ebb780..de9d7577 100644 --- a/src/domains/domains.spec.ts +++ b/src/domains/domains.spec.ts @@ -25,6 +25,7 @@ describe('Domains', () => { const response: CreateDomainResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', name: 'resend.com', + capability: 'send-and-receive', created_at: '2023-04-07T22:48:33.420498+00:00', status: 'not_started', records: [ @@ -69,6 +70,15 @@ describe('Domains', () => { status: 'not_started', ttl: 'Auto', }, + { + record: 'Receiving', + name: 'resend.com', + value: 'inbound-mx.resend.com', + type: 'MX', + ttl: 'Auto', + status: 'not_started', + priority: 10, + }, ], region: 'us-east-1', }; @@ -87,6 +97,7 @@ describe('Domains', () => { ).resolves.toMatchInlineSnapshot(` { "data": { + "capability": "send-and-receive", "created_at": "2023-04-07T22:48:33.420498+00:00", "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", "name": "resend.com", @@ -132,6 +143,15 @@ describe('Domains', () => { "type": "CNAME", "value": "eeaemodxoao5hxwjvhywx4bo5mswjw6v.dkim.com.", }, + { + "name": "resend.com", + "priority": 10, + "record": "Receiving", + "status": "not_started", + "ttl": "Auto", + "type": "MX", + "value": "inbound-mx.resend.com", + }, ], "region": "us-east-1", "status": "not_started", @@ -179,6 +199,7 @@ describe('Domains', () => { const response: CreateDomainResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', name: 'resend.com', + capability: 'send', created_at: '2023-04-07T22:48:33.420498+00:00', status: 'not_started', records: [ @@ -238,6 +259,7 @@ describe('Domains', () => { ).resolves.toMatchInlineSnapshot(` { "data": { + "capability": "send", "created_at": "2023-04-07T22:48:33.420498+00:00", "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", "name": "resend.com", @@ -330,6 +352,7 @@ describe('Domains', () => { const response: CreateDomainResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', name: 'resend.com', + capability: 'send', created_at: '2023-04-07T22:48:33.420498+00:00', status: 'not_started', records: [ @@ -381,6 +404,7 @@ describe('Domains', () => { ).resolves.toMatchInlineSnapshot(` { "data": { + "capability": "send", "created_at": "2023-04-07T22:48:33.420498+00:00", "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", "name": "resend.com", @@ -432,6 +456,7 @@ describe('Domains', () => { status: 'not_started', created_at: '2023-04-07T23:13:52.669661+00:00', region: 'eu-west-1', + capability: 'send', }, { id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', @@ -439,6 +464,7 @@ describe('Domains', () => { status: 'not_started', created_at: '2023-04-07T23:13:20.417116+00:00', region: 'us-east-1', + capability: 'receive', }, ], }; @@ -576,6 +602,7 @@ describe('Domains', () => { object: 'domain', id: 'fd61172c-cafc-40f5-b049-b45947779a29', name: 'resend.com', + capability: 'send-and-receive', status: 'not_started', created_at: '2023-06-21T06:10:36.144Z', region: 'us-east-1', @@ -606,6 +633,15 @@ describe('Domains', () => { status: 'verified', ttl: 'Auto', }, + { + record: 'Receiving', + name: 'resend.com', + value: 'inbound-mx.resend.com', + type: 'MX', + ttl: 'Auto', + status: 'not_started', + priority: 10, + }, ], }; @@ -622,6 +658,7 @@ describe('Domains', () => { await expect(resend.domains.get('1234')).resolves.toMatchInlineSnapshot(` { "data": { + "capability": "send-and-receive", "created_at": "2023-06-21T06:10:36.144Z", "id": "fd61172c-cafc-40f5-b049-b45947779a29", "name": "resend.com", @@ -652,6 +689,15 @@ describe('Domains', () => { "type": "TXT", "value": "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZDhdsAKs5xdSj7h3v22wjx3WMWWADCHwxfef8U03JUbVM/sNSVuY5mbrdJKUoG6QBdfxsOGzhINmQnT89idjp5GdAUhx/KNpt8hcLXMID4nB0Gbcafn03/z5zEPxPfzVJqQd/UqOtZQcfxN9OrIhLiBsYTbcTBB7EvjCb3wEaBwIDAQAB", }, + { + "name": "resend.com", + "priority": 10, + "record": "Receiving", + "status": "not_started", + "ttl": "Auto", + "type": "MX", + "value": "inbound-mx.resend.com", + }, ], "region": "us-east-1", "status": "not_started", diff --git a/src/domains/interfaces/create-domain-options.interface.ts b/src/domains/interfaces/create-domain-options.interface.ts index 13ba9562..e25ef789 100644 --- a/src/domains/interfaces/create-domain-options.interface.ts +++ b/src/domains/interfaces/create-domain-options.interface.ts @@ -6,12 +6,16 @@ export interface CreateDomainOptions { name: string; region?: DomainRegion; customReturnPath?: string; + capability?: 'send' | 'receive' | 'send-and-receive'; } export interface CreateDomainRequestOptions extends PostOptions {} export interface CreateDomainResponseSuccess - extends Pick { + extends Pick< + Domain, + 'name' | 'id' | 'status' | 'created_at' | 'region' | 'capability' + > { records: DomainRecords[]; } diff --git a/src/domains/interfaces/domain.ts b/src/domains/interfaces/domain.ts index 5bfce1c8..9b389fd6 100644 --- a/src/domains/interfaces/domain.ts +++ b/src/domains/interfaces/domain.ts @@ -21,7 +21,10 @@ export type DomainStatus = | 'temporary_failure' | 'not_started'; -export type DomainRecords = DomainSpfRecord | DomainDkimRecord; +export type DomainRecords = + | DomainSpfRecord + | DomainDkimRecord + | ReceivingRecord; export interface DomainSpfRecord { record: 'SPF'; @@ -47,10 +50,21 @@ export interface DomainDkimRecord { proxy_status?: 'enable' | 'disable'; } +export interface ReceivingRecord { + record: 'Receiving'; + name: string; + value: string; + type: 'MX'; + ttl: string; + status: DomainStatus; + priority: number; +} + export interface Domain { id: string; name: string; status: DomainStatus; created_at: string; region: DomainRegion; + capability: 'send' | 'receive' | 'send-and-receive'; } diff --git a/src/domains/interfaces/get-domain.interface.ts b/src/domains/interfaces/get-domain.interface.ts index 6c79410f..e9e681b1 100644 --- a/src/domains/interfaces/get-domain.interface.ts +++ b/src/domains/interfaces/get-domain.interface.ts @@ -2,7 +2,10 @@ import type { ErrorResponse } from '../../interfaces'; import type { Domain, DomainRecords } from './domain'; export interface GetDomainResponseSuccess - extends Pick { + extends Pick< + Domain, + 'id' | 'name' | 'created_at' | 'region' | 'status' | 'capability' + > { object: 'domain'; records: DomainRecords[]; } From 4747aeacec7f40df9b1dcc89b2c1e5bb96caaa49 Mon Sep 17 00:00:00 2001 From: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:26:22 +0000 Subject: [PATCH 27/49] feat: add verify webhooks (#636) --- src/resend.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/resend.ts b/src/resend.ts index 2d2eacd1..41add762 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -40,6 +40,7 @@ export class Resend { readonly webhooks = new Webhooks(); readonly templates = new Templates(this); readonly topics = new Topics(this); + readonly webhooks = new Webhooks(); constructor(readonly key?: string) { if (!key) { From def00a1058dd7510084e4ebc2004c646efff4c73 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Fri, 17 Oct 2025 10:55:17 -0300 Subject: [PATCH 28/49] feat: avoid downloading attachments ourselves (#685) --- .../receiving/interfaces/attachment.ts | 9 - .../interfaces/get-attachment.interface.ts | 7 +- src/attachments/receiving/receiving.spec.ts | 161 ++++-------------- src/attachments/receiving/receiving.ts | 111 +----------- 4 files changed, 39 insertions(+), 249 deletions(-) diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts index 0f003ad1..e04179ab 100644 --- a/src/attachments/receiving/interfaces/attachment.ts +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -1,13 +1,4 @@ export interface InboundAttachment { - id: string; - filename?: string; - content_type: string; - content_disposition: 'inline' | 'attachment'; - content_id?: string; - content: string; // base64 -} - -export interface ApiInboundAttachment { id: string; filename?: string; content_type: string; diff --git a/src/attachments/receiving/interfaces/get-attachment.interface.ts b/src/attachments/receiving/interfaces/get-attachment.interface.ts index 7bb34439..4c10e879 100644 --- a/src/attachments/receiving/interfaces/get-attachment.interface.ts +++ b/src/attachments/receiving/interfaces/get-attachment.interface.ts @@ -1,16 +1,11 @@ import type { ErrorResponse } from '../../../interfaces'; -import type { ApiInboundAttachment, InboundAttachment } from './attachment'; +import type { InboundAttachment } from './attachment'; export interface GetAttachmentOptions { emailId: string; id: string; } -export interface GetAttachmentApiResponse { - object: 'attachment'; - data: ApiInboundAttachment; -} - export interface GetAttachmentResponseSuccess { object: 'attachment'; data: InboundAttachment; diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts index e25fc905..74253f2b 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -12,18 +12,6 @@ fetchMocker.enableMocks(); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); -const pdfContent = 'PDF document content'; -const pdfBuffer = Buffer.from(pdfContent); -const pdfBase64 = pdfBuffer.toString('base64'); - -const imageContent = 'PNG image data'; -const imageBuffer = Buffer.from(imageContent); -const imageBase64 = imageBuffer.toString('base64'); - -const textContent = 'Plain text content'; -const textBuffer = Buffer.from(textContent); -const textBase64 = textBuffer.toString('base64'); - describe('Receiving', () => { afterEach(() => fetchMock.resetMocks()); afterAll(() => fetchMocker.disableMocks()); @@ -62,7 +50,7 @@ describe('Receiving', () => { }); describe('when attachment found', () => { - it('returns attachment with transformed fields', async () => { + it('returns attachment with download URL', async () => { const apiResponse = { object: 'attachment' as const, data: { @@ -72,6 +60,7 @@ describe('Receiving', () => { content_id: 'cid_123', content_disposition: 'attachment' as const, download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, }; @@ -87,14 +76,6 @@ describe('Receiving', () => { }, ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_123', @@ -103,10 +84,11 @@ describe('Receiving', () => { expect(result).toEqual({ data: { data: { - content: pdfBase64, content_disposition: 'attachment', content_id: 'cid_123', content_type: 'application/pdf', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', filename: 'document.pdf', id: 'att_123', }, @@ -116,7 +98,7 @@ describe('Receiving', () => { }); }); - it('returns inline attachment', async () => { + it('returns inline attachment with download URL', async () => { const apiResponse = { object: 'attachment' as const, data: { @@ -126,6 +108,7 @@ describe('Receiving', () => { content_id: 'cid_456', content_disposition: 'inline' as const, download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, }; @@ -141,14 +124,6 @@ describe('Receiving', () => { }, ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_456', @@ -157,10 +132,11 @@ describe('Receiving', () => { expect(result).toEqual({ data: { data: { - content: imageBase64, content_disposition: 'inline', content_id: 'cid_456', content_type: 'image/png', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', filename: 'image.png', id: 'att_456', }, @@ -179,6 +155,7 @@ describe('Receiving', () => { content_type: 'text/plain', content_disposition: 'attachment' as const, download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', // Optional fields (filename, content_id) omitted }, }; @@ -195,14 +172,6 @@ describe('Receiving', () => { }, ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_789', - async () => - new Response(textBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.get({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', id: 'att_789', @@ -211,9 +180,10 @@ describe('Receiving', () => { expect(result).toEqual({ data: { data: { - content: textBase64, content_disposition: 'attachment', content_type: 'text/plain', + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', id: 'att_789', }, object: 'attachment', @@ -236,6 +206,7 @@ describe('Receiving', () => { content_id: 'cid_123', content_disposition: 'attachment' as const, download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -244,6 +215,7 @@ describe('Receiving', () => { content_id: 'cid_456', content_disposition: 'inline' as const, download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -276,7 +248,7 @@ describe('Receiving', () => { }); describe('when attachments found', () => { - it('returns multiple attachments', async () => { + it('returns multiple attachments with download URLs', async () => { fetchMock.mockOnceIf( 'https://api.resend.com/emails/receiving/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', JSON.stringify(apiResponse), @@ -289,21 +261,6 @@ describe('Receiving', () => { }, ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); @@ -318,7 +275,8 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', - content: pdfBase64, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -326,7 +284,8 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', - content: imageBase64, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -367,21 +326,6 @@ describe('Receiving', () => { headers, }); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', }); @@ -396,7 +340,8 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', - content: pdfBase64, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -404,7 +349,8 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', - content: imageBase64, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -423,21 +369,6 @@ describe('Receiving', () => { it('calls endpoint passing limit param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', limit: 10, @@ -453,7 +384,8 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', - content: pdfBase64, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -461,7 +393,8 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', - content: imageBase64, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -478,21 +411,6 @@ describe('Receiving', () => { it('calls endpoint passing after param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', after: 'cursor123', @@ -508,7 +426,8 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', - content: pdfBase64, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -516,7 +435,8 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', - content: imageBase64, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -533,21 +453,6 @@ describe('Receiving', () => { it('calls endpoint passing before param and return the response', async () => { mockSuccessResponse(apiResponse, { headers }); - fetchMock.mockOnceIf( - 'https://example.com/download/att_123', - async () => - new Response(pdfBuffer, { - status: 200, - }), - ); - fetchMock.mockOnceIf( - 'https://example.com/download/att_456', - async () => - new Response(imageBuffer, { - status: 200, - }), - ); - const result = await resend.attachments.receiving.list({ emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', before: 'cursor123', @@ -563,7 +468,8 @@ describe('Receiving', () => { content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', - content: pdfBase64, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -571,7 +477,8 @@ describe('Receiving', () => { content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', - content: imageBase64, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, ], }; diff --git a/src/attachments/receiving/receiving.ts b/src/attachments/receiving/receiving.ts index 439a54db..8a7ab310 100644 --- a/src/attachments/receiving/receiving.ts +++ b/src/attachments/receiving/receiving.ts @@ -1,7 +1,6 @@ import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { - GetAttachmentApiResponse, GetAttachmentOptions, GetAttachmentResponse, GetAttachmentResponseSuccess, @@ -12,84 +11,17 @@ import type { ListAttachmentsResponse, } from './interfaces/list-attachments.interface'; -type DownloadAttachmentResult = - | { - type: 'error'; - message: string; - } - | { - type: 'success'; - base64Content: string; - }; - export class Receiving { constructor(private readonly resend: Resend) {} - private async downloadAttachment( - url: string, - ): Promise { - try { - const content = await fetch(url); - if (!content.ok) { - return { - type: 'error', - message: 'Failed to download attachment content', - }; - } - - const arrayBuffer = await content.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - return { - type: 'success', - base64Content: buffer.toString('base64'), - }; - } catch { - return { - type: 'error', - message: 'An error occurred while downloading attachment content', - }; - } - } - async get(options: GetAttachmentOptions): Promise { const { emailId, id } = options; - const apiResponse = await this.resend.get( + const data = await this.resend.get( `/emails/receiving/${emailId}/attachments/${id}`, ); - if ('error' in apiResponse && apiResponse.error) { - return apiResponse; - } - - const { - expires_at: _expires_at, - download_url, - ...otherFields - } = apiResponse.data.data; - const downloadResult = await this.downloadAttachment(download_url); - if (downloadResult.type === 'error') { - return { - data: null, - error: { - name: 'application_error', - message: downloadResult.message, - }, - }; - } - - const responseData: GetAttachmentResponseSuccess = { - object: 'attachment', - data: { - ...otherFields, - content: downloadResult.base64Content, - }, - }; - - return { - data: responseData, - error: null, - }; + return data; } async list( @@ -102,43 +34,8 @@ export class Receiving { ? `/emails/receiving/${emailId}/attachments?${queryString}` : `/emails/receiving/${emailId}/attachments`; - const apiResponse = await this.resend.get(url); - - if ('error' in apiResponse && apiResponse.error) { - return apiResponse; - } - - const attachmentsWithContent = []; - for (const attachment of apiResponse.data.data) { - const { - expires_at: _expires_at, - download_url, - ...otherFields - } = attachment; - const downloadResult = await this.downloadAttachment(download_url); - if (downloadResult.type === 'error') { - return { - data: null, - error: { - name: 'application_error', - message: downloadResult.message, - }, - }; - } - - attachmentsWithContent.push({ - ...otherFields, - content: downloadResult.base64Content, - }); - } + const data = await this.resend.get(url); - return { - data: { - object: 'list', - has_more: apiResponse.data.has_more, - data: attachmentsWithContent, - }, - error: null, - }; + return data; } } From de2e0e3a63f36622905d991fa3a80a4400845bb3 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Fri, 17 Oct 2025 11:45:24 -0300 Subject: [PATCH 29/49] chore: bump to canary 6.3.0-canary.1 (#687) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddb5b962..cbcce849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.3.0-canary.0", + "version": "6.3.0-canary.1", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From f6b5102b4f90c61bfad5c35c3c48234683822531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Fri, 17 Oct 2025 18:11:18 -0300 Subject: [PATCH 30/49] chore: limited variable types (#692) Co-authored-by: Lucas da Costa Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Vitor Capretz Co-authored-by: Gabriel Miranda Co-authored-by: Ty Mick <5317080+TyMick@users.noreply.github.com> Co-authored-by: Isabella Aquino Co-authored-by: Alexandre Cisneiros Co-authored-by: Carolina de Moraes Josephik <32900257+CarolinaMoraes@users.noreply.github.com> Co-authored-by: Carolina de Moraes Josephik Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Vitor Capretz Co-authored-by: Cassio Zen Co-authored-by: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com> Co-authored-by: Zeno Rocha --- .../create-template-options.interface.ts | 18 +-- src/templates/interfaces/template.ts | 16 +- .../interfaces/update-template.interface.ts | 18 +-- src/templates/templates.spec.ts | 141 +++--------------- 4 files changed, 24 insertions(+), 169 deletions(-) diff --git a/src/templates/interfaces/create-template-options.interface.ts b/src/templates/interfaces/create-template-options.interface.ts index 92548b28..e05fe5b0 100644 --- a/src/templates/interfaces/create-template-options.interface.ts +++ b/src/templates/interfaces/create-template-options.interface.ts @@ -1,11 +1,7 @@ import type { PostOptions } from '../../common/interfaces'; import type { RequireAtLeastOne } from '../../common/interfaces/require-at-least-one'; import type { ErrorResponse } from '../../interfaces'; -import type { - Template, - TemplateVariable, - TemplateVariableListFallbackType, -} from './template'; +import type { Template, TemplateVariable } from './template'; type TemplateContentCreationOptions = RequireAtLeastOne<{ html: string; @@ -22,18 +18,6 @@ type TemplateVariableCreationOptions = Pick & type: 'number'; fallbackValue?: number | null; } - | { - type: 'boolean'; - fallbackValue?: boolean | null; - } - | { - type: 'object'; - fallbackValue: Record; - } - | { - type: 'list'; - fallbackValue: TemplateVariableListFallbackType; - } ); type TemplateOptionalFieldsForCreation = Partial< diff --git a/src/templates/interfaces/template.ts b/src/templates/interfaces/template.ts index 94dc740b..af36e42a 100644 --- a/src/templates/interfaces/template.ts +++ b/src/templates/interfaces/template.ts @@ -16,22 +16,10 @@ export interface Template { current_version_id: string; } -export type TemplateVariableListFallbackType = - | string[] - | number[] - | boolean[] - | Record[]; - export interface TemplateVariable { key: string; - fallback_value: - | string - | number - | boolean - | Record - | TemplateVariableListFallbackType - | null; - type: 'string' | 'number' | 'boolean' | 'object' | 'list'; + fallback_value: string | number | null; + type: 'string' | 'number'; created_at: string; updated_at: string; } diff --git a/src/templates/interfaces/update-template.interface.ts b/src/templates/interfaces/update-template.interface.ts index f1b5275e..c3b6708b 100644 --- a/src/templates/interfaces/update-template.interface.ts +++ b/src/templates/interfaces/update-template.interface.ts @@ -1,9 +1,5 @@ import type { ErrorResponse } from '../../interfaces'; -import type { - Template, - TemplateVariable, - TemplateVariableListFallbackType, -} from './template'; +import type { Template, TemplateVariable } from './template'; type TemplateVariableUpdateOptions = Pick & ( @@ -15,18 +11,6 @@ type TemplateVariableUpdateOptions = Pick & type: 'number'; fallbackValue?: number | null; } - | { - type: 'boolean'; - fallbackValue?: boolean | null; - } - | { - type: 'object'; - fallbackValue: Record; - } - | { - type: 'list'; - fallbackValue: TemplateVariableListFallbackType; - } ); export interface UpdateTemplateOptions diff --git a/src/templates/templates.spec.ts b/src/templates/templates.spec.ts index 9aacfd5d..0f85d384 100644 --- a/src/templates/templates.spec.ts +++ b/src/templates/templates.spec.ts @@ -264,61 +264,6 @@ describe('Templates', () => { 'Failed to render React component. Make sure to install `@react-email/render`', ); }); - - it('creates a template with object and list variable types', async () => { - const payload: CreateTemplateOptions = { - name: 'Complex Variables Template', - html: '

Welcome {{{userProfile.name}}}!

Your tags: {{{tags}}}

', - variables: [ - { - key: 'userProfile', - type: 'object', - fallbackValue: { name: 'John', age: 30 }, - }, - { - key: 'tags', - type: 'list', - fallbackValue: ['premium', 'vip'], - }, - { - key: 'scores', - type: 'list', - fallbackValue: [95, 87, 92], - }, - { - key: 'flags', - type: 'list', - fallbackValue: [true, false, true], - }, - { - key: 'items', - type: 'list', - fallbackValue: [{ id: 1 }, { id: 2 }], - }, - ], - }; - const response: CreateTemplateResponseSuccess = { - object: 'template', - id: 'fd61172c-cafc-40f5-b049-b45947779a29', - }; - - mockSuccessResponse(response, { - headers: { Authorization: `Bearer ${TEST_API_KEY}` }, - }); - - const resend = new Resend(TEST_API_KEY); - await expect( - resend.templates.create(payload), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "id": "fd61172c-cafc-40f5-b049-b45947779a29", - "object": "template", - }, - "error": null, - } - `); - }); }); describe('remove', () => { @@ -541,65 +486,6 @@ describe('Templates', () => { } `); }); - - it('updates a template with object and list variable types', async () => { - const id = 'fd61172c-cafc-40f5-b049-b45947779a29'; - const payload: UpdateTemplateOptions = { - name: 'Updated Complex Variables Template', - html: '

Updated Welcome {{{config.theme}}}!

Permissions: {{{permissions}}}

', - variables: [ - { - key: 'config', - type: 'object', - fallbackValue: { theme: 'dark', lang: 'en' }, - }, - { - key: 'permissions', - type: 'list', - fallbackValue: ['read', 'write'], - }, - { - key: 'counts', - type: 'list', - fallbackValue: [10, 20, 30], - }, - { - key: 'enabled', - type: 'list', - fallbackValue: [true, false], - }, - { - key: 'metadata', - type: 'list', - fallbackValue: [{ key: 'a' }, { key: 'b' }], - }, - ], - }; - const response = { - object: 'template', - id, - }; - - mockSuccessResponse(response, { - headers: { - Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', - }, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - - await expect( - resend.templates.update(id, payload), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "id": "fd61172c-cafc-40f5-b049-b45947779a29", - "object": "template", - }, - "error": null, - } - `); - }); }); describe('get', () => { @@ -1015,20 +901,33 @@ describe('Templates', () => { const resend = new Resend(TEST_API_KEY); + // Test before and limit together await resend.templates.list({ before: 'cursor1', + limit: 25, + }); + + // Verify before and limit pagination parameters are included + const [firstUrl] = fetchMock.mock.calls[0]; + const firstParsedUrl = new URL(firstUrl as string); + + expect(firstParsedUrl.pathname).toBe('/templates'); + expect(firstParsedUrl.searchParams.get('before')).toBe('cursor1'); + expect(firstParsedUrl.searchParams.get('limit')).toBe('25'); + + // Test after and limit together + await resend.templates.list({ after: 'cursor2', limit: 25, }); - // Verify all pagination parameters are included - const [url] = fetchMock.mock.calls[0]; - const parsedUrl = new URL(url as string); + // Verify after and limit pagination parameters are included + const [secondUrl] = fetchMock.mock.calls[1]; + const secondParsedUrl = new URL(secondUrl as string); - expect(parsedUrl.pathname).toBe('/templates'); - expect(parsedUrl.searchParams.get('before')).toBe('cursor1'); - expect(parsedUrl.searchParams.get('after')).toBe('cursor2'); - expect(parsedUrl.searchParams.get('limit')).toBe('25'); + expect(secondParsedUrl.pathname).toBe('/templates'); + expect(secondParsedUrl.searchParams.get('after')).toBe('cursor2'); + expect(secondParsedUrl.searchParams.get('limit')).toBe('25'); }); }); }); From f0ccb0e36b6d3e8c125c02320016660f167afa07 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Fri, 17 Oct 2025 18:37:53 -0300 Subject: [PATCH 31/49] fix: remove duplicated webhook import --- src/resend.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resend.ts b/src/resend.ts index 41add762..2d2eacd1 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -40,7 +40,6 @@ export class Resend { readonly webhooks = new Webhooks(); readonly templates = new Templates(this); readonly topics = new Topics(this); - readonly webhooks = new Webhooks(); constructor(readonly key?: string) { if (!key) { From 8d6cb553f8c11a52f5770ee25d009cc414c57186 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 20 Oct 2025 10:13:20 -0300 Subject: [PATCH 32/49] fix: missing `statusCode` in ErrorRespnse type (#567) --- src/contacts/audiences/contact-audiences.spec.ts | 3 +++ src/contacts/audiences/contact-audiences.ts | 3 +++ src/contacts/topics/contact-topics.spec.ts | 2 ++ src/contacts/topics/contact-topics.ts | 2 ++ src/emails/emails.spec.ts | 2 ++ src/topics/topics.spec.ts | 9 +++++++++ src/topics/topics.ts | 3 +++ 7 files changed, 24 insertions(+) diff --git a/src/contacts/audiences/contact-audiences.spec.ts b/src/contacts/audiences/contact-audiences.spec.ts index 4fbb93ef..291cad5a 100644 --- a/src/contacts/audiences/contact-audiences.spec.ts +++ b/src/contacts/audiences/contact-audiences.spec.ts @@ -129,6 +129,7 @@ describe('ContactAudiences', () => { "error": { "message": "Missing \`id\` or \`email\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); @@ -204,6 +205,7 @@ describe('ContactAudiences', () => { "error": { "message": "Missing \`id\` or \`email\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); @@ -283,6 +285,7 @@ describe('ContactAudiences', () => { "error": { "message": "Missing \`id\` or \`email\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); diff --git a/src/contacts/audiences/contact-audiences.ts b/src/contacts/audiences/contact-audiences.ts index e1254c77..e743ca10 100644 --- a/src/contacts/audiences/contact-audiences.ts +++ b/src/contacts/audiences/contact-audiences.ts @@ -27,6 +27,7 @@ export class ContactAudiences { data: null, error: { message: 'Missing `id` or `email` field.', + statusCode: null, name: 'missing_required_field', }, }; @@ -51,6 +52,7 @@ export class ContactAudiences { data: null, error: { message: 'Missing `id` or `email` field.', + statusCode: null, name: 'missing_required_field', }, }; @@ -70,6 +72,7 @@ export class ContactAudiences { data: null, error: { message: 'Missing `id` or `email` field.', + statusCode: null, name: 'missing_required_field', }, }; diff --git a/src/contacts/topics/contact-topics.spec.ts b/src/contacts/topics/contact-topics.spec.ts index 93fdcba7..20a9f875 100644 --- a/src/contacts/topics/contact-topics.spec.ts +++ b/src/contacts/topics/contact-topics.spec.ts @@ -136,6 +136,7 @@ describe('ContactTopics', () => { "error": { "message": "Missing \`id\` or \`email\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); @@ -298,6 +299,7 @@ describe('ContactTopics', () => { "error": { "message": "Missing \`id\` or \`email\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); diff --git a/src/contacts/topics/contact-topics.ts b/src/contacts/topics/contact-topics.ts index 1eb15b0c..e87bd4a4 100644 --- a/src/contacts/topics/contact-topics.ts +++ b/src/contacts/topics/contact-topics.ts @@ -22,6 +22,7 @@ export class ContactTopics { data: null, error: { message: 'Missing `id` or `email` field.', + statusCode: null, name: 'missing_required_field', }, }; @@ -44,6 +45,7 @@ export class ContactTopics { data: null, error: { message: 'Missing `id` or `email` field.', + statusCode: null, name: 'missing_required_field', }, }; diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index f0eed3ec..d4df7587 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -558,6 +558,7 @@ describe('Emails', () => { const response: ErrorResponse = { name: 'not_found', message: 'Template not found', + statusCode: 404, }; fetchMock.mockOnce(JSON.stringify(response), { @@ -582,6 +583,7 @@ describe('Emails', () => { "error": { "message": "Template not found", "name": "not_found", + "statusCode": 404, }, } `); diff --git a/src/topics/topics.spec.ts b/src/topics/topics.spec.ts index b5978be8..62498b3d 100644 --- a/src/topics/topics.spec.ts +++ b/src/topics/topics.spec.ts @@ -57,6 +57,7 @@ describe('Topics', () => { const response: ErrorResponse = { name: 'missing_required_field', message: 'Missing `name` field.', + statusCode: 422, }; mockErrorResponse(response, { @@ -75,6 +76,7 @@ describe('Topics', () => { "error": { "message": "Missing \`name\` field.", "name": "missing_required_field", + "statusCode": 422, }, } `); @@ -88,6 +90,7 @@ describe('Topics', () => { const response: ErrorResponse = { name: 'missing_required_field', message: 'Missing `defaultSubscription` field.', + statusCode: 422, }; mockErrorResponse(response, { @@ -106,6 +109,7 @@ describe('Topics', () => { "error": { "message": "Missing \`defaultSubscription\` field.", "name": "missing_required_field", + "statusCode": 422, }, } `); @@ -170,6 +174,7 @@ describe('Topics', () => { const response: ErrorResponse = { name: 'not_found', message: 'Topic not found', + statusCode: 404, }; mockErrorResponse(response, { @@ -190,6 +195,7 @@ describe('Topics', () => { "error": { "message": "Topic not found", "name": "not_found", + "statusCode": 404, }, } `); @@ -236,6 +242,7 @@ describe('Topics', () => { "error": { "message": "Missing \`id\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); @@ -284,6 +291,7 @@ describe('Topics', () => { "error": { "message": "Missing \`id\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); @@ -326,6 +334,7 @@ describe('Topics', () => { "error": { "message": "Missing \`id\` field.", "name": "missing_required_field", + "statusCode": null, }, } `); diff --git a/src/topics/topics.ts b/src/topics/topics.ts index f46d9c1c..46d7a16c 100644 --- a/src/topics/topics.ts +++ b/src/topics/topics.ts @@ -48,6 +48,7 @@ export class Topics { data: null, error: { message: 'Missing `id` field.', + statusCode: null, name: 'missing_required_field', }, }; @@ -65,6 +66,7 @@ export class Topics { data: null, error: { message: 'Missing `id` field.', + statusCode: null, name: 'missing_required_field', }, }; @@ -84,6 +86,7 @@ export class Topics { data: null, error: { message: 'Missing `id` field.', + statusCode: null, name: 'missing_required_field', }, }; From ac6889e2faf6b57cb63d77bc38094051a0c5ccf4 Mon Sep 17 00:00:00 2001 From: Alexandre Cisneiros Date: Tue, 21 Oct 2025 10:52:25 -0700 Subject: [PATCH 33/49] feat: introduce the /segment API endpoints (#689) --- src/audiences/audiences.ts | 61 ---- .../create-audience-options.interface.ts | 24 -- .../interfaces/get-audience.interface.ts | 17 - src/audiences/interfaces/index.ts | 5 - .../interfaces/remove-audience.interface.ts | 17 - src/broadcasts/broadcasts.spec.ts | 16 +- src/broadcasts/broadcasts.ts | 2 + src/broadcasts/interfaces/broadcast.ts | 1 + .../create-broadcast-options.interface.ts | 18 +- .../interfaces/list-broadcasts.interface.ts | 1 + .../interfaces/update-broadcast.interface.ts | 4 + .../recording.har | 88 ++--- .../recording.har | 32 +- .../recording.har | 116 +++--- .../recording.har | 120 +++---- .../recording.har | 84 ++--- .../recording.har | 260 +++++++------- .../recording.har | 260 +++++++------- .../recording.har | 84 ++--- .../recording.har | 136 +++---- .../recording.har | 140 ++++---- .../recording.har | 148 ++++---- .../add-contact-audience.interface.ts | 20 -- .../list-contact-audiences.interface.ts | 22 -- .../remove-contact-audience.interface.ts | 21 -- src/contacts/contacts.integration.spec.ts | 2 +- src/contacts/contacts.ts | 6 +- .../contact-segments.spec.ts} | 92 ++--- .../contact-segments.ts} | 53 ++- .../add-contact-segment.interface.ts | 20 ++ .../interfaces/contact-segments.interface.ts} | 2 +- .../list-contact-segments.interface.ts | 22 ++ .../remove-contact-segment.interface.ts | 21 ++ src/index.ts | 2 +- src/resend.ts | 8 +- .../recording.har | 74 ++-- .../recording.har | 26 +- .../recording.har | 104 +++--- .../recording.har | 26 +- .../recording.har | 34 +- .../recording.har | 96 ++--- .../recording.har | 229 ++++++++++++ .../recording.har | 122 +++++++ .../recording.har | 336 ++++++++++++++++++ .../recording.har | 121 +++++++ .../recording.har | 121 +++++++ .../recording.har | 336 ++++++++++++++++++ .../audiences.integration.spec.ts | 6 +- src/{audiences => segments}/audiences.spec.ts | 44 +-- .../create-segment-options.interface.ts | 24 ++ .../interfaces/get-segment.interface.ts | 17 + src/segments/interfaces/index.ts | 5 + .../interfaces/list-segments.interface.ts} | 12 +- .../interfaces/remove-segment.interface.ts | 17 + .../interfaces/segment.ts} | 2 +- src/segments/segments.integration.spec.ts | 151 ++++++++ src/segments/segments.spec.ts | 291 +++++++++++++++ src/segments/segments.ts | 59 +++ 58 files changed, 2953 insertions(+), 1225 deletions(-) delete mode 100644 src/audiences/audiences.ts delete mode 100644 src/audiences/interfaces/create-audience-options.interface.ts delete mode 100644 src/audiences/interfaces/get-audience.interface.ts delete mode 100644 src/audiences/interfaces/index.ts delete mode 100644 src/audiences/interfaces/remove-audience.interface.ts delete mode 100644 src/contacts/audiences/interfaces/add-contact-audience.interface.ts delete mode 100644 src/contacts/audiences/interfaces/list-contact-audiences.interface.ts delete mode 100644 src/contacts/audiences/interfaces/remove-contact-audience.interface.ts rename src/contacts/{audiences/contact-audiences.spec.ts => segments/contact-segments.spec.ts} (71%) rename src/contacts/{audiences/contact-audiences.ts => segments/contact-segments.ts} (52%) create mode 100644 src/contacts/segments/interfaces/add-contact-segment.interface.ts rename src/contacts/{audiences/interfaces/contact-audiences.interface.ts => segments/interfaces/contact-segments.interface.ts} (73%) create mode 100644 src/contacts/segments/interfaces/list-contact-segments.interface.ts create mode 100644 src/contacts/segments/interfaces/remove-contact-segment.interface.ts rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har (73%) rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har (84%) rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har (74%) rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har (83%) rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har (76%) rename src/{audiences => segments}/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har (77%) create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har create mode 100644 src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har rename src/{audiences => segments}/audiences.integration.spec.ts (96%) rename src/{audiences => segments}/audiences.spec.ts (86%) create mode 100644 src/segments/interfaces/create-segment-options.interface.ts create mode 100644 src/segments/interfaces/get-segment.interface.ts create mode 100644 src/segments/interfaces/index.ts rename src/{audiences/interfaces/list-audiences.interface.ts => segments/interfaces/list-segments.interface.ts} (51%) create mode 100644 src/segments/interfaces/remove-segment.interface.ts rename src/{audiences/interfaces/audience.ts => segments/interfaces/segment.ts} (65%) create mode 100644 src/segments/segments.integration.spec.ts create mode 100644 src/segments/segments.spec.ts create mode 100644 src/segments/segments.ts diff --git a/src/audiences/audiences.ts b/src/audiences/audiences.ts deleted file mode 100644 index ae0427b3..00000000 --- a/src/audiences/audiences.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import type { Resend } from '../resend'; -import type { - CreateAudienceOptions, - CreateAudienceRequestOptions, - CreateAudienceResponse, - CreateAudienceResponseSuccess, -} from './interfaces/create-audience-options.interface'; -import type { - GetAudienceResponse, - GetAudienceResponseSuccess, -} from './interfaces/get-audience.interface'; -import type { - ListAudiencesOptions, - ListAudiencesResponse, - ListAudiencesResponseSuccess, -} from './interfaces/list-audiences.interface'; -import type { - RemoveAudiencesResponse, - RemoveAudiencesResponseSuccess, -} from './interfaces/remove-audience.interface'; - -export class Audiences { - constructor(private readonly resend: Resend) {} - - async create( - payload: CreateAudienceOptions, - options: CreateAudienceRequestOptions = {}, - ): Promise { - const data = await this.resend.post( - '/audiences', - payload, - options, - ); - return data; - } - - async list( - options: ListAudiencesOptions = {}, - ): Promise { - const queryString = buildPaginationQuery(options); - const url = queryString ? `/audiences?${queryString}` : '/audiences'; - - const data = await this.resend.get(url); - return data; - } - - async get(id: string): Promise { - const data = await this.resend.get( - `/audiences/${id}`, - ); - return data; - } - - async remove(id: string): Promise { - const data = await this.resend.delete( - `/audiences/${id}`, - ); - return data; - } -} diff --git a/src/audiences/interfaces/create-audience-options.interface.ts b/src/audiences/interfaces/create-audience-options.interface.ts deleted file mode 100644 index f8775dba..00000000 --- a/src/audiences/interfaces/create-audience-options.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { PostOptions } from '../../common/interfaces'; -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface CreateAudienceOptions { - name: string; -} - -export interface CreateAudienceRequestOptions extends PostOptions {} - -export interface CreateAudienceResponseSuccess - extends Pick { - object: 'audience'; -} - -export type CreateAudienceResponse = - | { - data: CreateAudienceResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/audiences/interfaces/get-audience.interface.ts b/src/audiences/interfaces/get-audience.interface.ts deleted file mode 100644 index 9c08efac..00000000 --- a/src/audiences/interfaces/get-audience.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface GetAudienceResponseSuccess - extends Pick { - object: 'audience'; -} - -export type GetAudienceResponse = - | { - data: GetAudienceResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/audiences/interfaces/index.ts b/src/audiences/interfaces/index.ts deleted file mode 100644 index 0aecd9db..00000000 --- a/src/audiences/interfaces/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './audience'; -export * from './create-audience-options.interface'; -export * from './get-audience.interface'; -export * from './list-audiences.interface'; -export * from './remove-audience.interface'; diff --git a/src/audiences/interfaces/remove-audience.interface.ts b/src/audiences/interfaces/remove-audience.interface.ts deleted file mode 100644 index e82e0b39..00000000 --- a/src/audiences/interfaces/remove-audience.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface RemoveAudiencesResponseSuccess extends Pick { - object: 'audience'; - deleted: boolean; -} - -export type RemoveAudiencesResponse = - | { - data: RemoveAudiencesResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index a1f73a6e..605f30dc 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -63,7 +63,7 @@ describe('Broadcasts', () => { const payload: CreateBroadcastOptions = { from: 'bu@resend.com', - audienceId: '0192f4ed-c2e9-7112-9c13-b04a043e23ee', + segmentId: '0192f4ed-c2e9-7112-9c13-b04a043e23ee', subject: 'Hello World', html: '

Hello world

', }; @@ -94,7 +94,7 @@ describe('Broadcasts', () => { const payload: CreateBroadcastOptions = { from: 'admin@resend.com', - audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917', + segmentId: '0192f4f1-d5f9-7110-8eb5-370552515917', subject: 'Hello World', text: 'Hello world', }; @@ -124,7 +124,7 @@ describe('Broadcasts', () => { const payload: CreateBroadcastOptions = { from: 'admin@resend.com', - audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917', + segmentId: '0192f4f1-d5f9-7110-8eb5-370552515917', replyTo: ['foo@resend.com', 'bar@resend.com'], subject: 'Hello World', text: 'Hello world', @@ -159,7 +159,7 @@ describe('Broadcasts', () => { const payload: CreateBroadcastOptions = { from: 'resend.com', // Invalid from address - audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917', + segmentId: '0192f4f1-d5f9-7110-8eb5-370552515917', replyTo: ['foo@resend.com', 'bar@resend.com'], subject: 'Hello World', text: 'Hello world', @@ -188,7 +188,7 @@ describe('Broadcasts', () => { const result = await resend.broadcasts.create({ from: 'example@resend.com', - audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917', + segmentId: '0192f4f1-d5f9-7110-8eb5-370552515917', subject: 'Hello World', text: 'Hello world', }); @@ -216,7 +216,7 @@ describe('Broadcasts', () => { const result = await resend.broadcasts.create({ from: 'example@resend.com', - audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917', + segmentId: '0192f4f1-d5f9-7110-8eb5-370552515917', subject: 'Hello World', text: 'Hello world', }); @@ -271,6 +271,7 @@ describe('Broadcasts', () => { { id: '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794', audience_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', + segment_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', name: 'broadcast 1', status: 'draft', created_at: '2024-11-01T15:13:31.723Z', @@ -280,6 +281,7 @@ describe('Broadcasts', () => { { id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', audience_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', + segment_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', name: 'broadcast 2', status: 'sent', created_at: '2024-12-01T19:32:22.980Z', @@ -426,6 +428,7 @@ describe('Broadcasts', () => { object: 'broadcast', id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', name: 'Announcements', + segment_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', audience_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', from: 'Acme ', html: '

Hello world

', @@ -465,6 +468,7 @@ describe('Broadcasts', () => { "preview_text": "Check out our latest announcements", "reply_to": null, "scheduled_at": null, + "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf", "sent_at": null, "status": "draft", "subject": "hello world", diff --git a/src/broadcasts/broadcasts.ts b/src/broadcasts/broadcasts.ts index 5092b7b2..2c383a2b 100644 --- a/src/broadcasts/broadcasts.ts +++ b/src/broadcasts/broadcasts.ts @@ -44,6 +44,7 @@ export class Broadcasts { '/broadcasts', { name: payload.name, + segment_id: payload.segmentId, audience_id: payload.audienceId, preview_text: payload.previewText, from: payload.from, @@ -107,6 +108,7 @@ export class Broadcasts { `/broadcasts/${id}`, { name: payload.name, + segment_id: payload.segmentId, audience_id: payload.audienceId, from: payload.from, html: payload.html, diff --git a/src/broadcasts/interfaces/broadcast.ts b/src/broadcasts/interfaces/broadcast.ts index d9a05a30..b8b49718 100644 --- a/src/broadcasts/interfaces/broadcast.ts +++ b/src/broadcasts/interfaces/broadcast.ts @@ -1,6 +1,7 @@ export interface Broadcast { id: string; name: string; + segment_id: string | null; audience_id: string | null; from: string | null; subject: string | null; diff --git a/src/broadcasts/interfaces/create-broadcast-options.interface.ts b/src/broadcasts/interfaces/create-broadcast-options.interface.ts index 73219884..38fb6bfa 100644 --- a/src/broadcasts/interfaces/create-broadcast-options.interface.ts +++ b/src/broadcasts/interfaces/create-broadcast-options.interface.ts @@ -24,19 +24,26 @@ interface EmailRenderOptions { text: string; } -interface CreateBroadcastBaseOptions { +interface SegmentOptions { /** - * The name of the broadcast + * The id of the segment you want to send to * * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters */ - name?: string; + segmentId: string; + /** + * @deprecated Use segmentId instead + */ + audienceId: string; +} + +interface CreateBroadcastBaseOptions { /** - * The id of the audience you want to send to + * The name of the broadcast * * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters */ - audienceId: string; + name?: string; /** * A short snippet of text displayed as a preview in recipients' inboxes, often shown below or beside the subject line. * @@ -70,6 +77,7 @@ interface CreateBroadcastBaseOptions { } export type CreateBroadcastOptions = RequireAtLeastOne & + RequireAtLeastOne & CreateBroadcastBaseOptions; export interface CreateBroadcastRequestOptions extends PostOptions {} diff --git a/src/broadcasts/interfaces/list-broadcasts.interface.ts b/src/broadcasts/interfaces/list-broadcasts.interface.ts index 8061427b..c600cc5d 100644 --- a/src/broadcasts/interfaces/list-broadcasts.interface.ts +++ b/src/broadcasts/interfaces/list-broadcasts.interface.ts @@ -12,6 +12,7 @@ export type ListBroadcastsResponseSuccess = { | 'id' | 'name' | 'audience_id' + | 'segment_id' | 'status' | 'created_at' | 'scheduled_at' diff --git a/src/broadcasts/interfaces/update-broadcast.interface.ts b/src/broadcasts/interfaces/update-broadcast.interface.ts index 388922ff..bc78728a 100644 --- a/src/broadcasts/interfaces/update-broadcast.interface.ts +++ b/src/broadcasts/interfaces/update-broadcast.interface.ts @@ -6,6 +6,10 @@ export interface UpdateBroadcastResponseSuccess { export type UpdateBroadcastOptions = { name?: string; + segmentId?: string; + /** + * @deprecated Use segmentId instead + */ audienceId?: string; from?: string; html?: string; diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har index 9c020abb..173b5068 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-create-creates-a-contact_2726386603/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "148c961dbf5fba70f1daa20e3380c096", + "_id": "665547b9aa413b822892a55e1667ce49", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for contact creation\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 109, "content": { "mimeType": "application/json; charset=utf-8", "size": 109, - "text": "{\"object\":\"audience\",\"id\":\"cc41cb0c-d74b-48cc-8829-82b7235f9480\",\"name\":\"Test audience for contact creation\"}" + "text": "{\"object\":\"audience\",\"id\":\"342faa8f-995c-4140-a424-737be8d1ced5\",\"name\":\"Test audience for contact creation\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b283c70e53d3b2-SEA" + "value": "99017480f839ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:36 GMT" + "value": "Fri, 17 Oct 2025 17:18:25 GMT" }, { "name": "etag", - "value": "W/\"6d-M3BoflhMDHltiRoXrhX6Fs8zP+c\"" + "value": "W/\"6d-jyZnjym7Jd2ZRJlgVDDG3P7hS6Q\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:35.841Z", - "time": 527, + "startedDateTime": "2025-10-17T17:18:25.333Z", + "time": 257, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 527 + "wait": 257 } }, { - "_id": "eebc33c2f1c289ef85b370c244579564", + "_id": "d510199548e03edeef157758fb721017", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test@example.com\",\"first_name\":\"Test\",\"last_name\":\"User\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/cc41cb0c-d74b-48cc-8829-82b7235f9480/contacts" + "url": "https://api.resend.com/audiences/342faa8f-995c-4140-a424-737be8d1ced5/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"95f4dd7b-661b-4d83-b00c-4161de562b45\"}" + "text": "{\"object\":\"contact\",\"id\":\"b7da9948-0951-43d4-88a2-69f336494fed\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b283cd6c68d3b2-SEA" + "value": "990174826e775913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:37 GMT" + "value": "Fri, 17 Oct 2025 17:18:25 GMT" }, { "name": "etag", - "value": "W/\"40-fOl6HaVbASpgeEMhS6Wn+WCb01I\"" + "value": "W/\"40-0YBQiM/UDsdlMPisb0sTJTN1dLI\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:36.972Z", - "time": 950, + "startedDateTime": "2025-10-17T17:18:25.592Z", + "time": 407, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 950 + "wait": 407 } }, { - "_id": "12d49bd04054ef1bcd6baaab1a54534b", + "_id": "80ce90fc77fe48120b255542f1fe1e29", "_order": 0, "cache": {}, "request": { @@ -241,21 +241,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/cc41cb0c-d74b-48cc-8829-82b7235f9480" + "url": "https://api.resend.com/segments/342faa8f-995c-4140-a424-737be8d1ced5" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"cc41cb0c-d74b-48cc-8829-82b7235f9480\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"342faa8f-995c-4140-a424-737be8d1ced5\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -265,7 +265,7 @@ }, { "name": "cf-ray", - "value": "98b283d71db7d3b2-SEA" + "value": "990174849cc5ad76-PDX" }, { "name": "connection", @@ -281,23 +281,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:38 GMT" + "value": "Fri, 17 Oct 2025 17:18:26 GMT" }, { "name": "etag", - "value": "W/\"50-aO5SoYBXdozT9SbLcSJaAxEXeVM\"" + "value": "W/\"50-ZLUepDaD1nIG5Xzqbw8ICWRnMAM\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -312,14 +312,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:38.528Z", - "time": 296, + "startedDateTime": "2025-10-17T17:18:26.000Z", + "time": 314, "timings": { "blocked": -1, "connect": -1, @@ -327,7 +327,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 296 + "wait": 314 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har index c3081e64..bf31a698 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-create-handles-validation-errors_3761275640/recording.har @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 201, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -40,11 +40,11 @@ "url": "https://api.resend.com/contacts" }, "response": { - "bodySize": 87, + "bodySize": 85, "content": { "mimeType": "application/json; charset=utf-8", - "size": 87, - "text": "{\"statusCode\":422,\"message\":\"The `id` must be a valid UUID.\",\"name\":\"validation_error\"}" + "size": 85, + "text": "{\"statusCode\":422,\"message\":\"Missing `email` field.\",\"name\":\"missing_required_field\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b283dccbb4d3b2-SEA" + "value": "990174869c35ad76-PDX" }, { "name": "connection", @@ -62,7 +62,7 @@ }, { "name": "content-length", - "value": "87" + "value": "85" }, { "name": "content-type", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:39 GMT" + "value": "Fri, 17 Oct 2025 17:18:26 GMT" }, { "name": "etag", - "value": "W/\"57-lOl5qyYLjWNv3C4rGuvDsfdJanQ\"" + "value": "W/\"55-3twEi8WnC2GKqaYdAl7cGsmeByc\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 422, "statusText": "Unprocessable Entity" }, - "startedDateTime": "2025-10-08T03:22:39.437Z", - "time": 129, + "startedDateTime": "2025-10-17T17:18:26.318Z", + "time": 154, "timings": { "blocked": -1, "connect": -1, @@ -112,7 +112,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 129 + "wait": 154 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har index a528bbaf..26102539 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-email_2183683318/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "df079127264d81dedd58026a51f771f5", + "_id": "a7148d90c9796babcc02e83f1bafa2fb", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for get by email\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 105, "content": { "mimeType": "application/json; charset=utf-8", "size": 105, - "text": "{\"object\":\"audience\",\"id\":\"789c8f84-d880-4cc8-85c6-1d1a07c20d42\",\"name\":\"Test audience for get by email\"}" + "text": "{\"object\":\"audience\",\"id\":\"b12a7fd8-7514-4893-8361-9ef21678dc4c\",\"name\":\"Test audience for get by email\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b28456792ad3b2-SEA" + "value": "990174aa8d29ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:59 GMT" + "value": "Fri, 17 Oct 2025 17:18:32 GMT" }, { "name": "etag", - "value": "W/\"69-tvCZvE/vttZEA21HdBaZ1lmqgmw\"" + "value": "W/\"69-mYlmY131iqLPmNWxIqBOUbAO/GU\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:58.909Z", - "time": 183, + "startedDateTime": "2025-10-17T17:18:32.073Z", + "time": 189, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 183 + "wait": 189 } }, { - "_id": "950f83a9b2efb96a89c93aca35e2336e", + "_id": "3258d18c3a0b32f88d3cb554fe5cce91", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test-get-by-email@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42/contacts" + "url": "https://api.resend.com/audiences/b12a7fd8-7514-4893-8361-9ef21678dc4c/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"f314b366-6c97-4e6d-abda-d667a69364ff\"}" + "text": "{\"object\":\"contact\",\"id\":\"ecfa803f-113f-4d65-b506-5885d692a21b\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b2845b5de4d3b2-SEA" + "value": "990174abccd65913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:59 GMT" + "value": "Fri, 17 Oct 2025 17:18:32 GMT" }, { "name": "etag", - "value": "W/\"40-42DGnIHcUqOQ5FXY5Qj0qGaAbKQ\"" + "value": "W/\"40-Oz0WE77GhPhjj1/hpRMX+ZT3mOo\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:59.694Z", - "time": 236, + "startedDateTime": "2025-10-17T17:18:32.263Z", + "time": 295, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 236 + "wait": 295 } }, { - "_id": "f402678be2de96dd7ddf838dd2075b1c", + "_id": "c5dcc049ae32086e3e156d07fbf9885d", "_order": 0, "cache": {}, "request": { @@ -241,21 +241,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 257, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42/contacts/test-get-by-email@example.com" + "url": "https://api.resend.com/audiences/b12a7fd8-7514-4893-8361-9ef21678dc4c/contacts/test-get-by-email@example.com" }, "response": { "bodySize": 205, "content": { "mimeType": "application/json; charset=utf-8", "size": 205, - "text": "{\"object\":\"contact\",\"id\":\"f314b366-6c97-4e6d-abda-d667a69364ff\",\"email\":\"test-get-by-email@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 23:09:05.075871+00\",\"unsubscribed\":false}" + "text": "{\"object\":\"contact\",\"id\":\"ecfa803f-113f-4d65-b506-5885d692a21b\",\"email\":\"test-get-by-email@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:14.626031+00\",\"unsubscribed\":false}" }, "cookies": [], "headers": [ @@ -265,7 +265,7 @@ }, { "name": "cf-ray", - "value": "98b28460aaf1d3b2-SEA" + "value": "990174ad9831ad76-PDX" }, { "name": "connection", @@ -281,23 +281,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:00 GMT" + "value": "Fri, 17 Oct 2025 17:18:32 GMT" }, { "name": "etag", - "value": "W/\"cd-3BWf4FmU/U0yarVYZXcpkdxvq44\"" + "value": "W/\"cd-IJhBNDZfJtgUskPzfvmcAOXC03Q\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -312,14 +312,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:00.533Z", - "time": 131, + "startedDateTime": "2025-10-17T17:18:32.559Z", + "time": 188, "timings": { "blocked": -1, "connect": -1, @@ -327,11 +327,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 131 + "wait": 188 } }, { - "_id": "07318a530c6056325fcbe71f649bd4e8", + "_id": "7e0a943177b9b8aa08d301f9324a8d0e", "_order": 0, "cache": {}, "request": { @@ -348,21 +348,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/789c8f84-d880-4cc8-85c6-1d1a07c20d42" + "url": "https://api.resend.com/segments/b12a7fd8-7514-4893-8361-9ef21678dc4c" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"789c8f84-d880-4cc8-85c6-1d1a07c20d42\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"b12a7fd8-7514-4893-8361-9ef21678dc4c\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -372,7 +372,7 @@ }, { "name": "cf-ray", - "value": "98b284653fc3d3b2-SEA" + "value": "990174aebc6dad76-PDX" }, { "name": "connection", @@ -388,23 +388,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:01 GMT" + "value": "Fri, 17 Oct 2025 17:18:32 GMT" }, { "name": "etag", - "value": "W/\"50-VbukeU9VzqKG4Zik8wmhPWliXaA\"" + "value": "W/\"50-myNtkcJEINsG49X2F2FiXGw1MFY\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -419,14 +419,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:01.266Z", - "time": 217, + "startedDateTime": "2025-10-17T17:18:32.747Z", + "time": 324, "timings": { "blocked": -1, "connect": -1, @@ -434,7 +434,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 217 + "wait": 324 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har index b06d0d4e..5c06463c 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-retrieves-a-contact-by-id_3532228743/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "fc71545e2243811f6588a5e3a0e1f326", + "_id": "ed88a0d65efc2ef06d7d97751b5d6c79", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for get by ID\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 102, "content": { "mimeType": "application/json; charset=utf-8", "size": 102, - "text": "{\"object\":\"audience\",\"id\":\"73ce0954-bc04-4656-bba4-e054600715ca\",\"name\":\"Test audience for get by ID\"}" + "text": "{\"object\":\"audience\",\"id\":\"8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d\",\"name\":\"Test audience for get by ID\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b28441ed7bd3b2-SEA" + "value": "990174a48e8bad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:55 GMT" + "value": "Fri, 17 Oct 2025 17:18:31 GMT" }, { "name": "etag", - "value": "W/\"66-KjuWnS1+tZJtY0w8MIEKMl7PFpg\"" + "value": "W/\"66-kY9l4F7yZU4E6WejINms8RmFE/Q\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:55.606Z", - "time": 210, + "startedDateTime": "2025-10-17T17:18:31.110Z", + "time": 156, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 210 + "wait": 156 } }, { - "_id": "a1c8b9e95dce5349cac37153431bde84", + "_id": "4b5fa999d8c434c106162f448bafe147", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test-get-by-id@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca/contacts" + "url": "https://api.resend.com/audiences/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"e260e001-cee2-4f2f-aa76-ba59e3d3b3b5\"}" + "text": "{\"object\":\"contact\",\"id\":\"5e26e74a-65db-452e-a6b6-a4fa4dd60afe\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b28446eaded3b2-SEA" + "value": "990174a588f55913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:56 GMT" + "value": "Fri, 17 Oct 2025 17:18:31 GMT" }, { "name": "etag", - "value": "W/\"40-sQeLQX2QW71rx/jFhozcwHDP/Wc\"" + "value": "W/\"40-2A3wqCIyroDRBQYGkrJpkl8H/rE\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:56.418Z", - "time": 320, + "startedDateTime": "2025-10-17T17:18:31.268Z", + "time": 264, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 320 + "wait": 264 } }, { - "_id": "2c30b18767715249d23abd8a3409dca8", + "_id": "e81aad142891d3dfbdbbf7b647cbb497", "_order": 0, "cache": {}, "request": { @@ -241,21 +241,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 264, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca/contacts/e260e001-cee2-4f2f-aa76-ba59e3d3b3b5" + "url": "https://api.resend.com/audiences/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d/contacts/5e26e74a-65db-452e-a6b6-a4fa4dd60afe" }, "response": { - "bodySize": 201, + "bodySize": 202, "content": { "mimeType": "application/json; charset=utf-8", - "size": 201, - "text": "{\"object\":\"contact\",\"id\":\"e260e001-cee2-4f2f-aa76-ba59e3d3b3b5\",\"email\":\"test-get-by-id@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:57:07.19709+00\",\"unsubscribed\":false}" + "size": 202, + "text": "{\"object\":\"contact\",\"id\":\"5e26e74a-65db-452e-a6b6-a4fa4dd60afe\",\"email\":\"test-get-by-id@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:12.007797+00\",\"unsubscribed\":false}" }, "cookies": [], "headers": [ @@ -265,7 +265,7 @@ }, { "name": "cf-ray", - "value": "98b2844ca85ed3b2-SEA" + "value": "990174a7292cad76-PDX" }, { "name": "connection", @@ -281,23 +281,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:57 GMT" + "value": "Fri, 17 Oct 2025 17:18:31 GMT" }, { "name": "etag", - "value": "W/\"c9-A1lZp26OVe28XOSbNYeCZSybgL0\"" + "value": "W/\"ca-bL+s+mrU1ZdXrP0HGVy1euucMjc\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "16" }, { "name": "ratelimit-reset", @@ -312,14 +312,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:57.341Z", - "time": 126, + "startedDateTime": "2025-10-17T17:18:31.534Z", + "time": 213, "timings": { "blocked": -1, "connect": -1, @@ -327,11 +327,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 126 + "wait": 213 } }, { - "_id": "6fce7e499edf8238e08d21bcc068d443", + "_id": "3f937b654e55ffbee0afb085b440a1a0", "_order": 0, "cache": {}, "request": { @@ -348,21 +348,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/73ce0954-bc04-4656-bba4-e054600715ca" + "url": "https://api.resend.com/segments/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"73ce0954-bc04-4656-bba4-e054600715ca\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -372,7 +372,7 @@ }, { "name": "cf-ray", - "value": "98b284513c9ad3b2-SEA" + "value": "990174a88df9ad76-PDX" }, { "name": "connection", @@ -388,23 +388,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:58 GMT" + "value": "Fri, 17 Oct 2025 17:18:31 GMT" }, { "name": "etag", - "value": "W/\"50-VAI2qkfcbIxTFjKbVkH99ebetpE\"" + "value": "W/\"50-ja5KH7a2OoHKanEhx4zGIynzfIs\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "15" }, { "name": "ratelimit-reset", @@ -419,14 +419,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:58.068Z", - "time": 237, + "startedDateTime": "2025-10-17T17:18:31.748Z", + "time": 318, "timings": { "blocked": -1, "connect": -1, @@ -434,7 +434,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 237 + "wait": 318 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har index d8404360..7c0ee293 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-get-returns-error-for-non-existent-contact_3399518725/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "a6f6a6bc9a75be64a528f2fb082fbfc9", + "_id": "efdfa668e0efb9cb39867c45bc2b2f4f", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for non-existent contact\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 113, "content": { "mimeType": "application/json; charset=utf-8", "size": 113, - "text": "{\"object\":\"audience\",\"id\":\"65bd037b-8048-448d-b9d8-153200097147\",\"name\":\"Test audience for non-existent contact\"}" + "text": "{\"object\":\"audience\",\"id\":\"f9f4a08d-51c6-485a-b89a-97ca2d999ee5\",\"name\":\"Test audience for non-existent contact\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b2846a5cf1d3b2-SEA" + "value": "990174b0cba3ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:02 GMT" + "value": "Fri, 17 Oct 2025 17:18:33 GMT" }, { "name": "etag", - "value": "W/\"71-XMmvodXAYDCobiSEj7raOLn2pP8\"" + "value": "W/\"71-KcnX6K2x2k8mhaatY2X239GxLFI\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "15" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:02.088Z", - "time": 138, + "startedDateTime": "2025-10-17T17:18:33.075Z", + "time": 231, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 138 + "wait": 231 } }, { - "_id": "51154edc062160334d2522051a872777", + "_id": "8c2ea5af757170ac609bbc43cfa6e26e", "_order": 0, "cache": {}, "request": { @@ -133,14 +133,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 264, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/65bd037b-8048-448d-b9d8-153200097147/contacts/00000000-0000-0000-0000-000000000000" + "url": "https://api.resend.com/audiences/f9f4a08d-51c6-485a-b89a-97ca2d999ee5/contacts/00000000-0000-0000-0000-000000000000" }, "response": { "bodySize": 67, @@ -157,7 +157,7 @@ }, { "name": "cf-ray", - "value": "98b2846ef958d3b2-SEA" + "value": "990174b2ba6a5913-PDX" }, { "name": "connection", @@ -173,7 +173,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:03 GMT" + "value": "Fri, 17 Oct 2025 17:18:33 GMT" }, { "name": "etag", @@ -181,15 +181,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "19" }, { "name": "ratelimit-reset", @@ -204,14 +204,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 404, "statusText": "Not Found" }, - "startedDateTime": "2025-10-08T03:23:02.829Z", - "time": 149, + "startedDateTime": "2025-10-17T17:18:33.308Z", + "time": 296, "timings": { "blocked": -1, "connect": -1, @@ -219,11 +219,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 149 + "wait": 296 } }, { - "_id": "426d3a38aac2c1de69a2b64f363dfef1", + "_id": "2aa04ee6117a8415f441929306418d5f", "_order": 0, "cache": {}, "request": { @@ -240,21 +240,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/65bd037b-8048-448d-b9d8-153200097147" + "url": "https://api.resend.com/segments/f9f4a08d-51c6-485a-b89a-97ca2d999ee5" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"65bd037b-8048-448d-b9d8-153200097147\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"f9f4a08d-51c6-485a-b89a-97ca2d999ee5\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -264,7 +264,7 @@ }, { "name": "cf-ray", - "value": "98b28473ae55d3b2-SEA" + "value": "990174b41ef4ad76-PDX" }, { "name": "connection", @@ -280,23 +280,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:03 GMT" + "value": "Fri, 17 Oct 2025 17:18:33 GMT" }, { "name": "etag", - "value": "W/\"50-jhMkz52LmanenOQbO761s7adex4\"" + "value": "W/\"50-XNsH+fU0j58WAyffrRj19tfwD4w\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -311,14 +311,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:03.582Z", - "time": 247, + "startedDateTime": "2025-10-17T17:18:33.605Z", + "time": 426, "timings": { "blocked": -1, "connect": -1, @@ -326,7 +326,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 247 + "wait": 426 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har index 4cfc7723..51056346 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-with-limit_871347592/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "0b85ff30f3862f9132a01f3b54d39212", + "_id": "27dc1ad8de477b198c1d2b9050b7fe03", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for listing with limit\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 111, "content": { "mimeType": "application/json; charset=utf-8", "size": 111, - "text": "{\"object\":\"audience\",\"id\":\"7152409b-1eb4-4300-96e8-f55e8df14c79\",\"name\":\"Test audience for listing with limit\"}" + "text": "{\"object\":\"audience\",\"id\":\"59e81eca-46ce-44d8-a8c7-6eb2c2350f30\",\"name\":\"Test audience for listing with limit\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b28411dda7d3b2-SEA" + "value": "990174958f8cad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:48 GMT" + "value": "Fri, 17 Oct 2025 17:18:28 GMT" }, { "name": "etag", - "value": "W/\"6f-yKILtqrdEbOctn20S2IgrMIXIqY\"" + "value": "W/\"6f-nj0fLhvMB7bgJqryScghpVt/OA0\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:47.926Z", - "time": 133, + "startedDateTime": "2025-10-17T17:18:28.714Z", + "time": 161, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 133 + "wait": 161 } }, { - "_id": "31ae6961ba3b639fa5adc520f5c09e3e", + "_id": "e281e196584e342749f9137ef5f52b5e", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test.0@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\"}" + "text": "{\"object\":\"contact\",\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b284166914d3b2-SEA" + "value": "99017496996b5913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:48 GMT" + "value": "Fri, 17 Oct 2025 17:18:29 GMT" }, { "name": "etag", - "value": "W/\"40-WcJL2Wka/X7v1Zq/rmJSt4fqpc8\"" + "value": "W/\"40-SPEIaxqwNgrPerxsVehA4Mc2YSw\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:48.661Z", - "time": 295, + "startedDateTime": "2025-10-17T17:18:28.877Z", + "time": 262, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 295 + "wait": 262 } }, { - "_id": "39bed937d9a89b4745d85b1a187a17a3", + "_id": "a2aa4d213b82b2d932f85021e08ff0a5", "_order": 0, "cache": {}, "request": { @@ -241,7 +241,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -253,14 +253,14 @@ "text": "{\"email\":\"test.1@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\"}" + "text": "{\"object\":\"contact\",\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\"}" }, "cookies": [], "headers": [ @@ -270,7 +270,7 @@ }, { "name": "cf-ray", - "value": "98b2841c0dc5d3b2-SEA" + "value": "990174983870ad76-PDX" }, { "name": "connection", @@ -286,23 +286,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:49 GMT" + "value": "Fri, 17 Oct 2025 17:18:29 GMT" }, { "name": "etag", - "value": "W/\"40-r0L5/NhwefMWKUlhD621pE9LFsw\"" + "value": "W/\"40-ypz1BtR9vGrNgplMnqFHflskWSQ\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -313,14 +313,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:49.559Z", - "time": 318, + "startedDateTime": "2025-10-17T17:18:29.140Z", + "time": 341, "timings": { "blocked": -1, "connect": -1, @@ -328,11 +328,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 318 + "wait": 341 } }, { - "_id": "f87da3869230f7aec485e05f9b12cf8f", + "_id": "d09a9532fa24a0ddb1f6fcf975c1440a", "_order": 0, "cache": {}, "request": { @@ -349,7 +349,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -361,14 +361,14 @@ "text": "{\"email\":\"test.2@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\"}" + "text": "{\"object\":\"contact\",\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\"}" }, "cookies": [], "headers": [ @@ -378,7 +378,7 @@ }, { "name": "cf-ray", - "value": "98b28421ca5cd3b2-SEA" + "value": "9901749a5cd15913-PDX" }, { "name": "connection", @@ -394,23 +394,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:50 GMT" + "value": "Fri, 17 Oct 2025 17:18:29 GMT" }, { "name": "etag", - "value": "W/\"40-DR4PbWnf0QFpFaMfKVAi4SYFl6Q\"" + "value": "W/\"40-eTTX5ZC9b/wIluwj/KM0k+9Rkik\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "16" }, { "name": "ratelimit-reset", @@ -421,14 +421,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:50.479Z", - "time": 321, + "startedDateTime": "2025-10-17T17:18:29.482Z", + "time": 255, "timings": { "blocked": -1, "connect": -1, @@ -436,11 +436,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 321 + "wait": 255 } }, { - "_id": "5418916ba0532e75d9397fd441feca1d", + "_id": "c2af0bd01bf47ea30352582ab9509c38", "_order": 0, "cache": {}, "request": { @@ -457,7 +457,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -469,14 +469,14 @@ "text": "{\"email\":\"test.3@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\"}" + "text": "{\"object\":\"contact\",\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\"}" }, "cookies": [], "headers": [ @@ -486,7 +486,7 @@ }, { "name": "cf-ray", - "value": "98b284278ec6d3b2-SEA" + "value": "9901749bfdd4ad76-PDX" }, { "name": "connection", @@ -502,23 +502,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:51 GMT" + "value": "Fri, 17 Oct 2025 17:18:30 GMT" }, { "name": "etag", - "value": "W/\"40-SBZrC974KMJ39cUl1r0nnWVOHUE\"" + "value": "W/\"40-nkeZzqF7RB7Sf8tLV2fEgsmcWeg\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "19" }, { "name": "ratelimit-reset", @@ -529,14 +529,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:51.403Z", - "time": 317, + "startedDateTime": "2025-10-17T17:18:29.738Z", + "time": 308, "timings": { "blocked": -1, "connect": -1, @@ -544,11 +544,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 317 + "wait": 308 } }, { - "_id": "6b100f0a4f9d1a2db01733f0f19f8d9d", + "_id": "5b18d5a0c5bdf29b7efa4fa48fb08929", "_order": 0, "cache": {}, "request": { @@ -565,7 +565,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -577,14 +577,14 @@ "text": "{\"email\":\"test.4@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\"}" + "text": "{\"object\":\"contact\",\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\"}" }, "cookies": [], "headers": [ @@ -594,7 +594,7 @@ }, { "name": "cf-ray", - "value": "98b2842d4b2bd3b2-SEA" + "value": "9901749dd8135913-PDX" }, { "name": "connection", @@ -610,23 +610,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:52 GMT" + "value": "Fri, 17 Oct 2025 17:18:30 GMT" }, { "name": "etag", - "value": "W/\"40-49pb2Z2Gv139gDXRXffI9uLXccU\"" + "value": "W/\"40-Ik+FFo//vczDSIdDQENIJWjKCUs\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -637,14 +637,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:52.323Z", - "time": 219, + "startedDateTime": "2025-10-17T17:18:30.047Z", + "time": 355, "timings": { "blocked": -1, "connect": -1, @@ -652,11 +652,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 219 + "wait": 355 } }, { - "_id": "1aaae92fb32a5ce9e90b6ab71a756a24", + "_id": "40e23df8de4e228681e77d206050f037", "_order": 0, "cache": {}, "request": { @@ -673,7 +673,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -685,14 +685,14 @@ "text": "{\"email\":\"test.5@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\"}" + "text": "{\"object\":\"contact\",\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\"}" }, "cookies": [], "headers": [ @@ -702,7 +702,7 @@ }, { "name": "cf-ray", - "value": "98b284326fb5d3b2-SEA" + "value": "990174a01d4aad76-PDX" }, { "name": "connection", @@ -718,23 +718,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:53 GMT" + "value": "Fri, 17 Oct 2025 17:18:30 GMT" }, { "name": "etag", - "value": "W/\"40-GK3+3/XcpMXwQrYY64qiPOWcO98\"" + "value": "W/\"40-W7N8ixsRtC8IRQU5RNbQ7dUAPrY\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -745,14 +745,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:53.144Z", - "time": 248, + "startedDateTime": "2025-10-17T17:18:30.403Z", + "time": 254, "timings": { "blocked": -1, "connect": -1, @@ -760,11 +760,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 248 + "wait": 254 } }, { - "_id": "24bbd56ea637eb4ae9ebf6bd589d0edf", + "_id": "6050a57b5c2fd774de74c96650746680", "_order": 0, "cache": {}, "request": { @@ -781,7 +781,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 235, @@ -793,14 +793,14 @@ "value": "5" } ], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79/contacts?limit=5" + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts?limit=5" }, "response": { - "bodySize": 920, + "bodySize": 921, "content": { "mimeType": "application/json; charset=utf-8", - "size": 920, - "text": "{\"object\":\"list\",\"has_more\":true,\"data\":[{\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:18.61266+00\",\"unsubscribed\":false},{\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.851518+00\",\"unsubscribed\":false},{\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.13096+00\",\"unsubscribed\":false},{\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:16.309282+00\",\"unsubscribed\":false},{\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:52.186404+00\",\"unsubscribed\":false}]}" + "size": 921, + "text": "{\"object\":\"list\",\"has_more\":true,\"data\":[{\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:07.461063+00\",\"unsubscribed\":false},{\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.973541+00\",\"unsubscribed\":false},{\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.271228+00\",\"unsubscribed\":false},{\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:04.32337+00\",\"unsubscribed\":false},{\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:03.608808+00\",\"unsubscribed\":false}]}" }, "cookies": [], "headers": [ @@ -810,7 +810,7 @@ }, { "name": "cf-ray", - "value": "98b28437cc6ad3b2-SEA" + "value": "990174a1bc125913-PDX" }, { "name": "connection", @@ -826,23 +826,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:54 GMT" + "value": "Fri, 17 Oct 2025 17:18:30 GMT" }, { "name": "etag", - "value": "W/\"398-fW0/lfZUazCpcdYtrpd+E01wfeo\"" + "value": "W/\"399-ZD+CAlt2/t3k/gEhTBP59XSLanY\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "16" }, { "name": "ratelimit-reset", @@ -857,14 +857,14 @@ "value": "chunked" } ], - "headersSize": 368, + "headersSize": 371, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:53.999Z", - "time": 138, + "startedDateTime": "2025-10-17T17:18:30.659Z", + "time": 154, "timings": { "blocked": -1, "connect": -1, @@ -872,11 +872,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 138 + "wait": 154 } }, { - "_id": "97f58aacbb5130621d9879553f478803", + "_id": "3b1c2334541543ace3170de6a4d80b80", "_order": 0, "cache": {}, "request": { @@ -893,21 +893,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/7152409b-1eb4-4300-96e8-f55e8df14c79" + "url": "https://api.resend.com/segments/59e81eca-46ce-44d8-a8c7-6eb2c2350f30" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"7152409b-1eb4-4300-96e8-f55e8df14c79\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"59e81eca-46ce-44d8-a8c7-6eb2c2350f30\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -917,7 +917,7 @@ }, { "name": "cf-ray", - "value": "98b2843c78c4d3b2-SEA" + "value": "990174a2aec3ad76-PDX" }, { "name": "connection", @@ -933,23 +933,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:54 GMT" + "value": "Fri, 17 Oct 2025 17:18:31 GMT" }, { "name": "etag", - "value": "W/\"50-sDJF/j+U5l3uff5MTKIEZmQXyCQ\"" + "value": "W/\"50-M9ULFfz+EKzIEGEXBm8C8Sd92r4\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "19" }, { "name": "ratelimit-reset", @@ -964,14 +964,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:54.741Z", - "time": 259, + "startedDateTime": "2025-10-17T17:18:30.815Z", + "time": 289, "timings": { "blocked": -1, "connect": -1, @@ -979,7 +979,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 259 + "wait": 289 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har index 80b6cddf..f4d374d2 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-list-lists-contacts-without-pagination_2611532587/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "5b20e71185afda27e38dc3d9b7f57624", + "_id": "d9c62769c0014e5e376db57af8c7614e", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for listing\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 100, "content": { "mimeType": "application/json; charset=utf-8", "size": 100, - "text": "{\"object\":\"audience\",\"id\":\"148a671b-3445-4d29-a79f-50e78b4b24ee\",\"name\":\"Test audience for listing\"}" + "text": "{\"object\":\"audience\",\"id\":\"92112b37-2b3c-4aeb-83fb-2f473fa4d740\",\"name\":\"Test audience for listing\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b283e1687dd3b2-SEA" + "value": "990174878f355913-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:40 GMT" + "value": "Fri, 17 Oct 2025 17:18:26 GMT" }, { "name": "etag", - "value": "W/\"64-AfDuQHSdpSb+3bU91Tphho+O/mk\"" + "value": "W/\"64-USObuVXxaszt+hyix9qHsXQUB0A\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:40.178Z", - "time": 133, + "startedDateTime": "2025-10-17T17:18:26.474Z", + "time": 159, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 133 + "wait": 159 } }, { - "_id": "e05dc1b2c3bcd17accf3d9b3bff793fd", + "_id": "018799e15322462f5bc40d79b52822be", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test.0@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\"}" + "text": "{\"object\":\"contact\",\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b283e5fce5d3b2-SEA" + "value": "990174888b07ad76-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:41 GMT" + "value": "Fri, 17 Oct 2025 17:18:26 GMT" }, { "name": "etag", - "value": "W/\"40-WcJL2Wka/X7v1Zq/rmJSt4fqpc8\"" + "value": "W/\"40-SPEIaxqwNgrPerxsVehA4Mc2YSw\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:40.914Z", - "time": 362, + "startedDateTime": "2025-10-17T17:18:26.634Z", + "time": 268, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 362 + "wait": 268 } }, { - "_id": "6446b997bf20c810f28c6bd2e0d32b45", + "_id": "160f1ba1f4266bf163c26002e15f195e", "_order": 0, "cache": {}, "request": { @@ -241,7 +241,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -253,14 +253,14 @@ "text": "{\"email\":\"test.1@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\"}" + "text": "{\"object\":\"contact\",\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\"}" }, "cookies": [], "headers": [ @@ -270,7 +270,7 @@ }, { "name": "cf-ray", - "value": "98b283ec0af5d3b2-SEA" + "value": "9901748a3fad5913-PDX" }, { "name": "connection", @@ -286,23 +286,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:42 GMT" + "value": "Fri, 17 Oct 2025 17:18:27 GMT" }, { "name": "etag", - "value": "W/\"40-r0L5/NhwefMWKUlhD621pE9LFsw\"" + "value": "W/\"40-ypz1BtR9vGrNgplMnqFHflskWSQ\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -313,14 +313,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:41.880Z", - "time": 256, + "startedDateTime": "2025-10-17T17:18:26.903Z", + "time": 327, "timings": { "blocked": -1, "connect": -1, @@ -328,11 +328,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 256 + "wait": 327 } }, { - "_id": "c818eda960f70164cbc07cb1cf5984d2", + "_id": "4614c250ed122a7ee4958dea76ad97c6", "_order": 0, "cache": {}, "request": { @@ -349,7 +349,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -361,14 +361,14 @@ "text": "{\"email\":\"test.2@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\"}" + "text": "{\"object\":\"contact\",\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\"}" }, "cookies": [], "headers": [ @@ -378,7 +378,7 @@ }, { "name": "cf-ray", - "value": "98b283f1691fd3b2-SEA" + "value": "9901748c4f60ad76-PDX" }, { "name": "connection", @@ -394,23 +394,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:43 GMT" + "value": "Fri, 17 Oct 2025 17:18:27 GMT" }, { "name": "etag", - "value": "W/\"40-DR4PbWnf0QFpFaMfKVAi4SYFl6Q\"" + "value": "W/\"40-eTTX5ZC9b/wIluwj/KM0k+9Rkik\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -421,14 +421,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:42.738Z", - "time": 279, + "startedDateTime": "2025-10-17T17:18:27.231Z", + "time": 281, "timings": { "blocked": -1, "connect": -1, @@ -436,11 +436,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 279 + "wait": 281 } }, { - "_id": "eb6c676c26223b34b9d520fd76539570", + "_id": "19c54bab554140a0c55d2e2b71fafef5", "_order": 0, "cache": {}, "request": { @@ -457,7 +457,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -469,14 +469,14 @@ "text": "{\"email\":\"test.3@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\"}" + "text": "{\"object\":\"contact\",\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\"}" }, "cookies": [], "headers": [ @@ -486,7 +486,7 @@ }, { "name": "cf-ray", - "value": "98b283f6ee1fd3b2-SEA" + "value": "9901748e0da85913-PDX" }, { "name": "connection", @@ -502,23 +502,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:43 GMT" + "value": "Fri, 17 Oct 2025 17:18:27 GMT" }, { "name": "etag", - "value": "W/\"40-SBZrC974KMJ39cUl1r0nnWVOHUE\"" + "value": "W/\"40-nkeZzqF7RB7Sf8tLV2fEgsmcWeg\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -529,14 +529,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:43.620Z", - "time": 265, + "startedDateTime": "2025-10-17T17:18:27.512Z", + "time": 248, "timings": { "blocked": -1, "connect": -1, @@ -544,11 +544,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 265 + "wait": 248 } }, { - "_id": "6b66b521adca931136b05dcb99eefb5d", + "_id": "9c330a59f1800cd2cb5d8a13f3bb4439", "_order": 0, "cache": {}, "request": { @@ -565,7 +565,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -577,14 +577,14 @@ "text": "{\"email\":\"test.4@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\"}" + "text": "{\"object\":\"contact\",\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\"}" }, "cookies": [], "headers": [ @@ -594,7 +594,7 @@ }, { "name": "cf-ray", - "value": "98b283fc5a96d3b2-SEA" + "value": "9901748f9ae2ad76-PDX" }, { "name": "connection", @@ -610,23 +610,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:44 GMT" + "value": "Fri, 17 Oct 2025 17:18:27 GMT" }, { "name": "etag", - "value": "W/\"40-49pb2Z2Gv139gDXRXffI9uLXccU\"" + "value": "W/\"40-Ik+FFo//vczDSIdDQENIJWjKCUs\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -637,14 +637,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:44.489Z", - "time": 269, + "startedDateTime": "2025-10-17T17:18:27.761Z", + "time": 238, "timings": { "blocked": -1, "connect": -1, @@ -652,11 +652,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 269 + "wait": 238 } }, { - "_id": "184123983e7f24eafe5bba4b05c4b19d", + "_id": "8c845d58009faa21e611093dd041f9f6", "_order": 0, "cache": {}, "request": { @@ -673,7 +673,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -685,14 +685,14 @@ "text": "{\"email\":\"test.5@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\"}" + "text": "{\"object\":\"contact\",\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\"}" }, "cookies": [], "headers": [ @@ -702,7 +702,7 @@ }, { "name": "cf-ray", - "value": "98b28401cf49d3b2-SEA" + "value": "990174911fc95913-PDX" }, { "name": "connection", @@ -718,23 +718,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:45 GMT" + "value": "Fri, 17 Oct 2025 17:18:28 GMT" }, { "name": "etag", - "value": "W/\"40-GK3+3/XcpMXwQrYY64qiPOWcO98\"" + "value": "W/\"40-W7N8ixsRtC8IRQU5RNbQ7dUAPrY\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -745,14 +745,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:22:45.361Z", - "time": 259, + "startedDateTime": "2025-10-17T17:18:28.001Z", + "time": 255, "timings": { "blocked": -1, "connect": -1, @@ -760,11 +760,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 259 + "wait": 255 } }, { - "_id": "1dcc6abb6ca116e4a00e870a2ff65ebd", + "_id": "2fbe917448561c27fcf7f4886a6c9b6a", "_order": 0, "cache": {}, "request": { @@ -781,21 +781,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 227, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee/contacts" + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" }, "response": { - "bodySize": 1097, + "bodySize": 1098, "content": { "mimeType": "application/json; charset=utf-8", - "size": 1097, - "text": "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"ee419ace-4e9c-4823-a12e-88e897eb0512\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:18.61266+00\",\"unsubscribed\":false},{\"id\":\"08ec3b2b-00c0-4ae2-b2d0-b72b8ca8c949\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.851518+00\",\"unsubscribed\":false},{\"id\":\"267a8ea6-533d-468e-89fe-a79b1c61542d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:17.13096+00\",\"unsubscribed\":false},{\"id\":\"9e7d6c88-cee5-4eb0-8d4a-3b7a44b4c3d0\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:42:16.309282+00\",\"unsubscribed\":false},{\"id\":\"cc1f925e-de49-406b-8e78-f1d056e91c32\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:52.186404+00\",\"unsubscribed\":false},{\"id\":\"bbe5b098-d2f4-46b1-b20c-433ade6a4bba\",\"email\":\"test.0@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-05 22:39:51.231744+00\",\"unsubscribed\":false}]}" + "size": 1098, + "text": "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:07.461063+00\",\"unsubscribed\":false},{\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.973541+00\",\"unsubscribed\":false},{\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.271228+00\",\"unsubscribed\":false},{\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:04.32337+00\",\"unsubscribed\":false},{\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:03.608808+00\",\"unsubscribed\":false},{\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\",\"email\":\"test.0@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:01.981048+00\",\"unsubscribed\":false}]}" }, "cookies": [], "headers": [ @@ -805,7 +805,7 @@ }, { "name": "cf-ray", - "value": "98b284073b9ed3b2-SEA" + "value": "99017492bdbdad76-PDX" }, { "name": "connection", @@ -821,23 +821,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:46 GMT" + "value": "Fri, 17 Oct 2025 17:18:28 GMT" }, { "name": "etag", - "value": "W/\"449-d7s8QRhoXU5ObVQnddszfMAKzgk\"" + "value": "W/\"44a-VNxzWns+IrIaVg1bxBGHZwFOPsI\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -852,14 +852,14 @@ "value": "chunked" } ], - "headersSize": 368, + "headersSize": 371, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:46.223Z", - "time": 129, + "startedDateTime": "2025-10-17T17:18:28.258Z", + "time": 201, "timings": { "blocked": -1, "connect": -1, @@ -867,11 +867,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 129 + "wait": 201 } }, { - "_id": "691bff31ec52e8b2802ce08750907042", + "_id": "5d335154a64b7a44a8c72229eb45da1e", "_order": 0, "cache": {}, "request": { @@ -888,21 +888,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/148a671b-3445-4d29-a79f-50e78b4b24ee" + "url": "https://api.resend.com/segments/92112b37-2b3c-4aeb-83fb-2f473fa4d740" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"148a671b-3445-4d29-a79f-50e78b4b24ee\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"92112b37-2b3c-4aeb-83fb-2f473fa4d740\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -912,7 +912,7 @@ }, { "name": "cf-ray", - "value": "98b2840bc807d3b2-SEA" + "value": "99017493fa45ad76-PDX" }, { "name": "connection", @@ -928,23 +928,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:22:47 GMT" + "value": "Fri, 17 Oct 2025 17:18:28 GMT" }, { "name": "etag", - "value": "W/\"50-qA69IAti3hYt4F1qPM4W79EcnF8\"" + "value": "W/\"50-/gYbLxfuFX9Iy0Byw/iyMwl/ySI\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "15" }, { "name": "ratelimit-reset", @@ -959,14 +959,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:22:46.957Z", - "time": 362, + "startedDateTime": "2025-10-17T17:18:28.460Z", + "time": 248, "timings": { "blocked": -1, "connect": -1, @@ -974,7 +974,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 362 + "wait": 248 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har index eb346658..637a16c7 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-appears-to-remove-a-contact-that-never-existed_2913832504/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "1c0a4af7474576bd5c1779e8e4af0cac", + "_id": "31be3e42567e9e6de269f27d108c276f", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for non-existent delete\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 112, "content": { "mimeType": "application/json; charset=utf-8", "size": 112, - "text": "{\"object\":\"audience\",\"id\":\"82ef6091-9a99-4687-8207-e20a65b9caa0\",\"name\":\"Test audience for non-existent delete\"}" + "text": "{\"object\":\"audience\",\"id\":\"929b3690-e56f-4f69-9f7d-0d4005b8fd50\",\"name\":\"Test audience for non-existent delete\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b284c7aa29d3b2-SEA" + "value": "990174d0fb0dad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:17 GMT" + "value": "Fri, 17 Oct 2025 17:18:38 GMT" }, { "name": "etag", - "value": "W/\"70-WGLvJbhrmbGv4qRnzhIVzB1MNqk\"" + "value": "W/\"70-MyhAdzWJr/jt7uIVtOIRMlY2yXA\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:17.015Z", - "time": 126, + "startedDateTime": "2025-10-17T17:18:38.219Z", + "time": 157, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 126 + "wait": 157 } }, { - "_id": "a19c42f86ca0928e2ef08a45d696e70b", + "_id": "b64b4983d1955e42ed83e2d0adb89468", "_order": 0, "cache": {}, "request": { @@ -133,14 +133,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 267, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/82ef6091-9a99-4687-8207-e20a65b9caa0/contacts/00000000-0000-0000-0000-000000000000" + "url": "https://api.resend.com/audiences/929b3690-e56f-4f69-9f7d-0d4005b8fd50/contacts/00000000-0000-0000-0000-000000000000" }, "response": { "bodySize": 84, @@ -157,7 +157,7 @@ }, { "name": "cf-ray", - "value": "98b284cc3f48d3b2-SEA" + "value": "990174d1eb1f5913-PDX" }, { "name": "connection", @@ -173,7 +173,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:17 GMT" + "value": "Fri, 17 Oct 2025 17:18:38 GMT" }, { "name": "etag", @@ -181,15 +181,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -204,14 +204,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:17.743Z", - "time": 136, + "startedDateTime": "2025-10-17T17:18:38.377Z", + "time": 163, "timings": { "blocked": -1, "connect": -1, @@ -219,11 +219,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 136 + "wait": 163 } }, { - "_id": "22d21e18b86b71ca50f45aee766e3595", + "_id": "19e03a717661326db0cbaf1b8b9b4762", "_order": 0, "cache": {}, "request": { @@ -240,21 +240,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/82ef6091-9a99-4687-8207-e20a65b9caa0" + "url": "https://api.resend.com/segments/929b3690-e56f-4f69-9f7d-0d4005b8fd50" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"82ef6091-9a99-4687-8207-e20a65b9caa0\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"929b3690-e56f-4f69-9f7d-0d4005b8fd50\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -264,7 +264,7 @@ }, { "name": "cf-ray", - "value": "98b284d0dca8d3b2-SEA" + "value": "990174d2fa22ad76-PDX" }, { "name": "connection", @@ -280,23 +280,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:18 GMT" + "value": "Fri, 17 Oct 2025 17:18:38 GMT" }, { "name": "etag", - "value": "W/\"50-zq6CQsEEf053K7ExinvVENpVmYY\"" + "value": "W/\"50-zhQy7WO8qBt7uw8FHWJFx8jonmE\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "15" }, { "name": "ratelimit-reset", @@ -311,14 +311,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:18.482Z", - "time": 402, + "startedDateTime": "2025-10-17T17:18:38.540Z", + "time": 212, "timings": { "blocked": -1, "connect": -1, @@ -326,7 +326,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 402 + "wait": 212 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har index 48448341..fd02682b 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-email_1973365032/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "26223e02c7e6371faa2f0ee268a6c27c", + "_id": "8e78d48529cfeb223b879f40f616663e", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for remove by email\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 108, "content": { "mimeType": "application/json; charset=utf-8", "size": 108, - "text": "{\"object\":\"audience\",\"id\":\"6084b604-13ba-4d48-8412-248ab5bbe237\",\"name\":\"Test audience for remove by email\"}" + "text": "{\"object\":\"audience\",\"id\":\"3810aa6d-856d-42e7-b1cb-05a3b21b8db6\",\"name\":\"Test audience for remove by email\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b284ad7cbfd3b2-SEA" + "value": "990174c95895ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:12 GMT" + "value": "Fri, 17 Oct 2025 17:18:37 GMT" }, { "name": "etag", - "value": "W/\"6c-2YvfE338LS5NqrbyiFmPlxmGJqc\"" + "value": "W/\"6c-74EOpd7v3g6M6bUJrQnDTopoZpk\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:12.822Z", - "time": 136, + "startedDateTime": "2025-10-17T17:18:37.004Z", + "time": 146, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 136 + "wait": 146 } }, { - "_id": "073e179e10f6201a270d033ac04d90c7", + "_id": "08b51636fb8cf0d312f13158cfbf9d99", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test-remove-by-email@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts" + "url": "https://api.resend.com/audiences/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"d3c8869a-9e4d-4d9d-88f8-78ee1c813e0a\"}" + "text": "{\"object\":\"contact\",\"id\":\"ff589dde-1acc-497a-b71d-736661a981fd\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b284b21a3dd3b2-SEA" + "value": "990174ca482f5913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:13 GMT" + "value": "Fri, 17 Oct 2025 17:18:37 GMT" }, { "name": "etag", - "value": "W/\"40-1sHkj5CP6O7fzmRQgy1H3A++5qA\"" + "value": "W/\"40-umpVh6I6kDpYeb5WAHwZdvNxA6g\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:13.563Z", - "time": 277, + "startedDateTime": "2025-10-17T17:18:37.150Z", + "time": 337, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 277 + "wait": 337 } }, { - "_id": "bbea9c09cb3801dc4f5b2b35a5559630", + "_id": "e48b0940904311dbe91c643b2e98bd50", "_order": 0, "cache": {}, "request": { @@ -241,14 +241,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 263, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts/test-remove-by-email@example.com" + "url": "https://api.resend.com/audiences/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/contacts/test-remove-by-email@example.com" }, "response": { "bodySize": 80, @@ -265,7 +265,7 @@ }, { "name": "cf-ray", - "value": "98b284b78849d3b2-SEA" + "value": "990174cc6b21ad76-PDX" }, { "name": "connection", @@ -281,7 +281,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:14 GMT" + "value": "Fri, 17 Oct 2025 17:18:37 GMT" }, { "name": "etag", @@ -289,15 +289,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -312,14 +312,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:14.444Z", - "time": 317, + "startedDateTime": "2025-10-17T17:18:37.487Z", + "time": 333, "timings": { "blocked": -1, "connect": -1, @@ -327,11 +327,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 317 + "wait": 333 } }, { - "_id": "24cb476b6b97bc1efef3d9861a406fda", + "_id": "d854344ff2e35283becd868efc86a911", "_order": 0, "cache": {}, "request": { @@ -348,14 +348,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 260, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237/contacts/test-remove-by-email@example.com" + "url": "https://api.resend.com/audiences/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/contacts/test-remove-by-email@example.com" }, "response": { "bodySize": 67, @@ -372,7 +372,7 @@ }, { "name": "cf-ray", - "value": "98b284bd4f13d3b2-SEA" + "value": "990174ce7a08ad76-PDX" }, { "name": "connection", @@ -388,7 +388,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:15 GMT" + "value": "Fri, 17 Oct 2025 17:18:37 GMT" }, { "name": "etag", @@ -396,15 +396,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -419,14 +419,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 404, "statusText": "Not Found" }, - "startedDateTime": "2025-10-08T03:23:15.364Z", - "time": 119, + "startedDateTime": "2025-10-17T17:18:37.821Z", + "time": 153, "timings": { "blocked": -1, "connect": -1, @@ -434,11 +434,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 119 + "wait": 153 } }, { - "_id": "cb48ac2cb33bbe8039a7d42f1c65345b", + "_id": "6c733407504f42f7f3939e22c93234e9", "_order": 0, "cache": {}, "request": { @@ -455,21 +455,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/6084b604-13ba-4d48-8412-248ab5bbe237" + "url": "https://api.resend.com/segments/3810aa6d-856d-42e7-b1cb-05a3b21b8db6" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"6084b604-13ba-4d48-8412-248ab5bbe237\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"3810aa6d-856d-42e7-b1cb-05a3b21b8db6\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -479,7 +479,7 @@ }, { "name": "cf-ray", - "value": "98b284c1cba7d3b2-SEA" + "value": "990174cf6d67ad76-PDX" }, { "name": "connection", @@ -495,23 +495,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:16 GMT" + "value": "Fri, 17 Oct 2025 17:18:38 GMT" }, { "name": "etag", - "value": "W/\"50-paTxZhN68xNQaMfYLkZ45Vuz9MI\"" + "value": "W/\"50-Zl3VlcxhpmxSBHvwOqi6U3gLgmw\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -526,14 +526,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:16.085Z", - "time": 319, + "startedDateTime": "2025-10-17T17:18:37.975Z", + "time": 242, "timings": { "blocked": -1, "connect": -1, @@ -541,7 +541,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 319 + "wait": 242 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har index bdd753e6..03b17d52 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-remove-removes-a-contact-by-id_3813073181/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "af2a077e2764b5b5f628794d35d3d720", + "_id": "e804c3a44a1cdbc7eacdf402a7b4f39e", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for remove by ID\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 105, "content": { "mimeType": "application/json; charset=utf-8", "size": 105, - "text": "{\"object\":\"audience\",\"id\":\"c16d061a-2009-4517-b361-c2b86ef1600a\",\"name\":\"Test audience for remove by ID\"}" + "text": "{\"object\":\"audience\",\"id\":\"70771159-5b4e-476e-9783-35bd76c5aea8\",\"name\":\"Test audience for remove by ID\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b2849259bdd3b2-SEA" + "value": "990174c0caf1ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:08 GMT" + "value": "Fri, 17 Oct 2025 17:18:35 GMT" }, { "name": "etag", - "value": "W/\"69-sNjp3MwpApGPS9iPF4oCewDgKG0\"" + "value": "W/\"69-N7156RrBa/10i7mNjcWTamF3nmU\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 338, + "headersSize": 341, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:08.494Z", - "time": 138, + "startedDateTime": "2025-10-17T17:18:35.571Z", + "time": 240, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 138 + "wait": 240 } }, { - "_id": "c27f5c20f0e03606e5c32a70adac0f9e", + "_id": "8fa48ffd03607392f9088eb5ad03e4ae", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test-remove-by-id@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts" + "url": "https://api.resend.com/audiences/70771159-5b4e-476e-9783-35bd76c5aea8/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"c60f145c-d15b-4dde-a80c-c42e9938d045\"}" + "text": "{\"object\":\"contact\",\"id\":\"a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b284970d66d3b2-SEA" + "value": "990174c1ebe75913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:09 GMT" + "value": "Fri, 17 Oct 2025 17:18:36 GMT" }, { "name": "etag", - "value": "W/\"40-2XMj4i9A4564FX7sT1LdpVF3kOA\"" + "value": "W/\"40-jfysRk4N8NfGNJcXomgbzIJCM+g\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:09.236Z", - "time": 404, + "startedDateTime": "2025-10-17T17:18:35.812Z", + "time": 343, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 404 + "wait": 343 } }, { - "_id": "6739fd118521c4510309cae2f5d4e977", + "_id": "175c65be846ad8604e0b79377b5ffda0", "_order": 0, "cache": {}, "request": { @@ -241,21 +241,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 267, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts/c60f145c-d15b-4dde-a80c-c42e9938d045" + "url": "https://api.resend.com/audiences/70771159-5b4e-476e-9783-35bd76c5aea8/contacts/a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d" }, "response": { "bodySize": 84, "content": { "mimeType": "application/json; charset=utf-8", "size": 84, - "text": "{\"object\":\"contact\",\"contact\":\"c60f145c-d15b-4dde-a80c-c42e9938d045\",\"deleted\":true}" + "text": "{\"object\":\"contact\",\"contact\":\"a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -265,7 +265,7 @@ }, { "name": "cf-ray", - "value": "98b2849d4b89d3b2-SEA" + "value": "990174c40e89ad76-PDX" }, { "name": "connection", @@ -281,23 +281,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:10 GMT" + "value": "Fri, 17 Oct 2025 17:18:36 GMT" }, { "name": "etag", - "value": "W/\"54-39hTgkIWR3XiORFIu2E1oNO8DkI\"" + "value": "W/\"54-xZ3FSZahyWRBkRDBEOXvp73ajdM\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -312,14 +312,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:10.244Z", - "time": 318, + "startedDateTime": "2025-10-17T17:18:36.157Z", + "time": 352, "timings": { "blocked": -1, "connect": -1, @@ -327,11 +327,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 318 + "wait": 352 } }, { - "_id": "51824a6c06054fe78f7edcc654458a06", + "_id": "edd0d9a66149d94815c7ce04827ba80b", "_order": 0, "cache": {}, "request": { @@ -348,14 +348,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 264, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a/contacts/c60f145c-d15b-4dde-a80c-c42e9938d045" + "url": "https://api.resend.com/audiences/70771159-5b4e-476e-9783-35bd76c5aea8/contacts/a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d" }, "response": { "bodySize": 67, @@ -372,7 +372,7 @@ }, { "name": "cf-ray", - "value": "98b284a30883d3b2-SEA" + "value": "990174c6e826ad76-PDX" }, { "name": "connection", @@ -388,7 +388,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:11 GMT" + "value": "Fri, 17 Oct 2025 17:18:36 GMT" }, { "name": "etag", @@ -396,15 +396,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -419,14 +419,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 404, "statusText": "Not Found" }, - "startedDateTime": "2025-10-08T03:23:11.164Z", - "time": 136, + "startedDateTime": "2025-10-17T17:18:36.510Z", + "time": 252, "timings": { "blocked": -1, "connect": -1, @@ -434,11 +434,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 136 + "wait": 252 } }, { - "_id": "67a911c82843a2f26a8a8388265852a0", + "_id": "a07617b83e6a4dc407533e0144d9e12b", "_order": 0, "cache": {}, "request": { @@ -455,21 +455,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/c16d061a-2009-4517-b361-c2b86ef1600a" + "url": "https://api.resend.com/segments/70771159-5b4e-476e-9783-35bd76c5aea8" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"c16d061a-2009-4517-b361-c2b86ef1600a\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"70771159-5b4e-476e-9783-35bd76c5aea8\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -479,7 +479,7 @@ }, { "name": "cf-ray", - "value": "98b284a7be38d3b2-SEA" + "value": "990174c7db80ad76-PDX" }, { "name": "connection", @@ -495,23 +495,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:12 GMT" + "value": "Fri, 17 Oct 2025 17:18:36 GMT" }, { "name": "etag", - "value": "W/\"50-5wvL7mfOj1d3krCoKv8e3OVuHaU\"" + "value": "W/\"50-7jG26XSURJ4Z0dn6tkuFlzd+srA\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -526,14 +526,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:11.903Z", - "time": 308, + "startedDateTime": "2025-10-17T17:18:36.764Z", + "time": 237, "timings": { "blocked": -1, "connect": -1, @@ -541,7 +541,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 308 + "wait": 237 } } ], diff --git a/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har b/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har index 35c995b3..30ff023e 100644 --- a/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har +++ b/src/contacts/__recordings__/Contacts-Integration-Tests-update-updates-a-contact_1708768821/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "67f4325e3459cfa40bc7bffaea69175a", + "_id": "7e436bec9db93ade6fd32ba82b86a603", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test audience for update\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 99, "content": { "mimeType": "application/json; charset=utf-8", "size": 99, - "text": "{\"object\":\"audience\",\"id\":\"546f6a6a-1407-4ed6-9f9e-061ff0e9f806\",\"name\":\"Test audience for update\"}" + "text": "{\"object\":\"audience\",\"id\":\"ab119ae4-44bf-4f3f-b809-235002703ebd\",\"name\":\"Test audience for update\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b284790b2dd3b2-SEA" + "value": "990174b6c817ad76-PDX" }, { "name": "connection", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:04 GMT" + "value": "Fri, 17 Oct 2025 17:18:34 GMT" }, { "name": "etag", - "value": "W/\"63-p/20qfR4eBJMmRhps2Z7c7ki6Yc\"" + "value": "W/\"63-VCultuXnDFeQp3w+Em+XiEg+DPk\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:04.436Z", - "time": 131, + "startedDateTime": "2025-10-17T17:18:34.036Z", + "time": 284, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 131 + "wait": 284 } }, { - "_id": "80d3a0659966c921b30b53ceb2041cc2", + "_id": "7fec48e2c530889d8db6605add34ce50", "_order": 0, "cache": {}, "request": { @@ -133,7 +133,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 228, @@ -145,14 +145,14 @@ "text": "{\"email\":\"test-update@example.com\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts" + "url": "https://api.resend.com/audiences/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\"}" + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\"}" }, "cookies": [], "headers": [ @@ -162,7 +162,7 @@ }, { "name": "cf-ray", - "value": "98b2847d9f75d3b2-SEA" + "value": "990174b93ffd5913-PDX" }, { "name": "connection", @@ -178,23 +178,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:05 GMT" + "value": "Fri, 17 Oct 2025 17:18:34 GMT" }, { "name": "etag", - "value": "W/\"40-oKY1bSWE4bkaVKE1zwa9IIHPDEY\"" + "value": "W/\"40-yAc35ksyId+b7UcvfJAExdFUHYE\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -205,14 +205,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:05.171Z", - "time": 374, + "startedDateTime": "2025-10-17T17:18:34.321Z", + "time": 450, "timings": { "blocked": -1, "connect": -1, @@ -220,11 +220,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 374 + "wait": 450 } }, { - "_id": "ed52e78f06ccb66b6d2ad8c57fef4458", + "_id": "2c9fa319407a8ec99d3a13f3a7b62e20", "_order": 0, "cache": {}, "request": { @@ -241,7 +241,7 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 266, @@ -253,14 +253,14 @@ "text": "{\"first_name\":\"Updated\",\"last_name\":\"Name\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts/b5955a5a-9dab-4e37-8b41-19c88a570bd9" + "url": "https://api.resend.com/audiences/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts/45bf1c68-22ec-4acd-90d3-06c4c4784cd4" }, "response": { "bodySize": 64, "content": { "mimeType": "application/json; charset=utf-8", "size": 64, - "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\"}" + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\"}" }, "cookies": [], "headers": [ @@ -270,7 +270,7 @@ }, { "name": "cf-ray", - "value": "98b28483bcd9d3b2-SEA" + "value": "990174bb6f7dad76-PDX" }, { "name": "connection", @@ -286,23 +286,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:06 GMT" + "value": "Fri, 17 Oct 2025 17:18:35 GMT" }, { "name": "etag", - "value": "W/\"40-oKY1bSWE4bkaVKE1zwa9IIHPDEY\"" + "value": "W/\"40-yAc35ksyId+b7UcvfJAExdFUHYE\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "18" }, { "name": "ratelimit-reset", @@ -317,14 +317,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:06.147Z", - "time": 210, + "startedDateTime": "2025-10-17T17:18:34.773Z", + "time": 265, "timings": { "blocked": -1, "connect": -1, @@ -332,11 +332,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 210 + "wait": 265 } }, { - "_id": "fe304c75966f38e76807b7de4db0eb7b", + "_id": "348f89902dbcc5e2615eda52449d93de", "_order": 0, "cache": {}, "request": { @@ -353,21 +353,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], "headersSize": 264, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806/contacts/b5955a5a-9dab-4e37-8b41-19c88a570bd9" + "url": "https://api.resend.com/audiences/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts/45bf1c68-22ec-4acd-90d3-06c4c4784cd4" }, "response": { - "bodySize": 206, + "bodySize": 205, "content": { "mimeType": "application/json; charset=utf-8", - "size": 206, - "text": "{\"object\":\"contact\",\"id\":\"b5955a5a-9dab-4e37-8b41-19c88a570bd9\",\"email\":\"test-update@example.com\",\"first_name\":\"Updated\",\"last_name\":\"Name\",\"created_at\":\"2025-10-05 23:29:21.371802+00\",\"unsubscribed\":false}" + "size": 205, + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\",\"email\":\"test-update@example.com\",\"first_name\":\"Updated\",\"last_name\":\"Name\",\"created_at\":\"2025-10-17 16:47:19.18697+00\",\"unsubscribed\":false}" }, "cookies": [], "headers": [ @@ -377,7 +377,7 @@ }, { "name": "cf-ray", - "value": "98b28488c9b5d3b2-SEA" + "value": "990174bd1da9ad76-PDX" }, { "name": "connection", @@ -393,23 +393,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:07 GMT" + "value": "Fri, 17 Oct 2025 17:18:35 GMT" }, { "name": "etag", - "value": "W/\"ce-P4qWHq31vVQIc0z4O/722nTfnyI\"" + "value": "W/\"cd-rNTm2DvbIH83YX1VRDiNRc+nyHo\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "17" }, { "name": "ratelimit-reset", @@ -424,14 +424,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:06.960Z", - "time": 125, + "startedDateTime": "2025-10-17T17:18:35.040Z", + "time": 171, "timings": { "blocked": -1, "connect": -1, @@ -439,11 +439,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 125 + "wait": 171 } }, { - "_id": "f694219f36b7c584d6326d4d144d0601", + "_id": "9a669484e44a6f4df4ee6ab609ac15ce", "_order": 0, "cache": {}, "request": { @@ -460,21 +460,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.0" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/546f6a6a-1407-4ed6-9f9e-061ff0e9f806" + "url": "https://api.resend.com/segments/ab119ae4-44bf-4f3f-b809-235002703ebd" }, "response": { "bodySize": 80, "content": { "mimeType": "application/json; charset=utf-8", "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"546f6a6a-1407-4ed6-9f9e-061ff0e9f806\",\"deleted\":true}" + "text": "{\"object\":\"audience\",\"id\":\"ab119ae4-44bf-4f3f-b809-235002703ebd\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -484,7 +484,7 @@ }, { "name": "cf-ray", - "value": "98b2848d5d74d3b2-SEA" + "value": "990174be2958ad76-PDX" }, { "name": "connection", @@ -500,23 +500,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:07 GMT" + "value": "Fri, 17 Oct 2025 17:18:35 GMT" }, { "name": "etag", - "value": "W/\"50-k56M8zygcH9DZCxRUkWndf+66xw\"" + "value": "W/\"50-C7lWpGGjIiJvyGOaVuojCUZ58hY\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "16" }, { "name": "ratelimit-reset", @@ -531,14 +531,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:07.687Z", - "time": 196, + "startedDateTime": "2025-10-17T17:18:35.212Z", + "time": 352, "timings": { "blocked": -1, "connect": -1, @@ -546,7 +546,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 196 + "wait": 352 } } ], diff --git a/src/contacts/audiences/interfaces/add-contact-audience.interface.ts b/src/contacts/audiences/interfaces/add-contact-audience.interface.ts deleted file mode 100644 index 419a130b..00000000 --- a/src/contacts/audiences/interfaces/add-contact-audience.interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ErrorResponse } from '../../../interfaces'; -import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; - -export type AddContactAudiencesOptions = ContactAudiencesBaseOptions & { - audienceId: string; -}; - -export interface AddContactAudiencesResponseSuccess { - id: string; -} - -export type AddContactAudiencesResponse = - | { - data: AddContactAudiencesResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts b/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts deleted file mode 100644 index 1b0a9e2f..00000000 --- a/src/contacts/audiences/interfaces/list-contact-audiences.interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Audience } from '../../../audiences/interfaces/audience'; -import type { - PaginatedData, - PaginationOptions, -} from '../../../common/interfaces/pagination-options.interface'; -import type { ErrorResponse } from '../../../interfaces'; -import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; - -export type ListContactAudiencesOptions = PaginationOptions & - ContactAudiencesBaseOptions; - -export type ListContactAudiencesResponseSuccess = PaginatedData; - -export type ListContactAudiencesResponse = - | { - data: ListContactAudiencesResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts b/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts deleted file mode 100644 index c6a1d322..00000000 --- a/src/contacts/audiences/interfaces/remove-contact-audience.interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ErrorResponse } from '../../../interfaces'; -import type { ContactAudiencesBaseOptions } from './contact-audiences.interface'; - -export type RemoveContactAudiencesOptions = ContactAudiencesBaseOptions & { - audienceId: string; -}; - -export interface RemoveContactAudiencesResponseSuccess { - id: string; - deleted: boolean; -} - -export type RemoveContactAudiencesResponse = - | { - data: RemoveContactAudiencesResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/contacts/contacts.integration.spec.ts b/src/contacts/contacts.integration.spec.ts index 09ab67ef..4bd9d9dc 100644 --- a/src/contacts/contacts.integration.spec.ts +++ b/src/contacts/contacts.integration.spec.ts @@ -45,7 +45,7 @@ describe('Contacts Integration Tests', () => { // @ts-expect-error: Testing invalid input const result = await resend.contacts.create({}); - expect(result.error?.name).toBe('validation_error'); + expect(result.error?.name).toBe('missing_required_field'); }); }); diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts index 05cd7a24..730be8fa 100644 --- a/src/contacts/contacts.ts +++ b/src/contacts/contacts.ts @@ -1,6 +1,5 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; import type { Resend } from '../resend'; -import { ContactAudiences } from './audiences/contact-audiences'; import type { CreateContactOptions, CreateContactRequestOptions, @@ -28,15 +27,16 @@ import type { UpdateContactResponse, UpdateContactResponseSuccess, } from './interfaces/update-contact.interface'; +import { ContactSegments } from './segments/contact-segments'; import { ContactTopics } from './topics/contact-topics'; export class Contacts { readonly topics: ContactTopics; - readonly audiences: ContactAudiences; + readonly segments: ContactSegments; constructor(private readonly resend: Resend) { this.topics = new ContactTopics(this.resend); - this.audiences = new ContactAudiences(this.resend); + this.segments = new ContactSegments(this.resend); } async create( diff --git a/src/contacts/audiences/contact-audiences.spec.ts b/src/contacts/segments/contact-segments.spec.ts similarity index 71% rename from src/contacts/audiences/contact-audiences.spec.ts rename to src/contacts/segments/contact-segments.spec.ts index 291cad5a..cdd9825a 100644 --- a/src/contacts/audiences/contact-audiences.spec.ts +++ b/src/contacts/segments/contact-segments.spec.ts @@ -2,40 +2,40 @@ import createFetchMock from 'vitest-fetch-mock'; import { Resend } from '../../resend'; import { mockSuccessResponse } from '../../test-utils/mock-fetch'; import type { - AddContactAudiencesOptions, - AddContactAudiencesResponseSuccess, -} from './interfaces/add-contact-audience.interface'; + AddContactSegmentOptions, + AddContactSegmentResponseSuccess, +} from './interfaces/add-contact-segment.interface'; import type { - ListContactAudiencesOptions, - ListContactAudiencesResponseSuccess, -} from './interfaces/list-contact-audiences.interface'; + ListContactSegmentsOptions, + ListContactSegmentsResponseSuccess, +} from './interfaces/list-contact-segments.interface'; import type { - RemoveContactAudiencesOptions, - RemoveContactAudiencesResponseSuccess, -} from './interfaces/remove-contact-audience.interface'; + RemoveContactSegmentOptions, + RemoveContactSegmentResponseSuccess, +} from './interfaces/remove-contact-segment.interface'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); -describe('ContactAudiences', () => { +describe('ContactSegments', () => { afterEach(() => fetchMock.resetMocks()); afterAll(() => fetchMocker.disableMocks()); describe('list', () => { - it('gets contact audiences by email', async () => { - const options: ListContactAudiencesOptions = { + it('gets contact segments by email', async () => { + const options: ListContactSegmentsOptions = { email: 'carolina@resend.com', }; - const response: ListContactAudiencesResponseSuccess = { + const response: ListContactSegmentsResponseSuccess = { object: 'list', data: [ { id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', - name: 'Test Audience', + name: 'Test Segment', created_at: '2021-01-01T00:00:00.000Z', }, { id: 'd7e1e488-ae2c-4255-a40c-a4db3af7ed0c', - name: 'Another Audience', + name: 'Another Segment', created_at: '2021-01-02T00:00:00.000Z', }, ], @@ -48,7 +48,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.list(options), + resend.contacts.segments.list(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -56,12 +56,12 @@ describe('ContactAudiences', () => { { "created_at": "2021-01-01T00:00:00.000Z", "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", - "name": "Test Audience", + "name": "Test Segment", }, { "created_at": "2021-01-02T00:00:00.000Z", "id": "d7e1e488-ae2c-4255-a40c-a4db3af7ed0c", - "name": "Another Audience", + "name": "Another Segment", }, ], "has_more": false, @@ -72,18 +72,18 @@ describe('ContactAudiences', () => { `); }); - it('gets contact audiences by ID', async () => { - const options: ListContactAudiencesOptions = { + it('gets contact segments by ID', async () => { + const options: ListContactSegmentsOptions = { contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', limit: 1, after: '584a472d-bc6d-4dd2-aa9d-d3d50ce87222', }; - const response: ListContactAudiencesResponseSuccess = { + const response: ListContactSegmentsResponseSuccess = { object: 'list', data: [ { id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', - name: 'Test Audience', + name: 'Test Segment', created_at: '2021-01-01T00:00:00.000Z', }, ], @@ -96,7 +96,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.list(options), + resend.contacts.segments.list(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -104,7 +104,7 @@ describe('ContactAudiences', () => { { "created_at": "2021-01-01T00:00:00.000Z", "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", - "name": "Test Audience", + "name": "Test Segment", }, ], "has_more": true, @@ -119,8 +119,8 @@ describe('ContactAudiences', () => { const options = {}; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = resend.contacts.audiences.list( - options as ListContactAudiencesOptions, + const result = resend.contacts.segments.list( + options as ListContactSegmentsOptions, ); await expect(result).resolves.toMatchInlineSnapshot(` @@ -138,12 +138,12 @@ describe('ContactAudiences', () => { describe('add', () => { it('adds a contact to an audience', async () => { - const options: AddContactAudiencesOptions = { + const options: AddContactSegmentOptions = { email: 'carolina@resend.com', - audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', }; - const response: AddContactAudiencesResponseSuccess = { + const response: AddContactSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', }; @@ -153,7 +153,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.add(options), + resend.contacts.segments.add(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -165,12 +165,12 @@ describe('ContactAudiences', () => { }); it('adds a contact to an audience by ID', async () => { - const options: AddContactAudiencesOptions = { + const options: AddContactSegmentOptions = { contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', - audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', }; - const response: AddContactAudiencesResponseSuccess = { + const response: AddContactSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', }; @@ -180,7 +180,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.add(options), + resend.contacts.segments.add(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -195,8 +195,8 @@ describe('ContactAudiences', () => { const options = {}; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = resend.contacts.audiences.add( - options as AddContactAudiencesOptions, + const result = resend.contacts.segments.add( + options as AddContactSegmentOptions, ); await expect(result).resolves.toMatchInlineSnapshot(` @@ -214,12 +214,12 @@ describe('ContactAudiences', () => { describe('remove', () => { it('removes a contact from an audience', async () => { - const options: RemoveContactAudiencesOptions = { + const options: RemoveContactSegmentOptions = { email: 'carolina@resend.com', - audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', }; - const response: RemoveContactAudiencesResponseSuccess = { + const response: RemoveContactSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', deleted: true, }; @@ -230,7 +230,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.remove(options), + resend.contacts.segments.remove(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -243,12 +243,12 @@ describe('ContactAudiences', () => { }); it('removes a contact from an audience by ID', async () => { - const options: RemoveContactAudiencesOptions = { + const options: RemoveContactSegmentOptions = { contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', - audienceId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', }; - const response: RemoveContactAudiencesResponseSuccess = { + const response: RemoveContactSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', deleted: true, }; @@ -259,7 +259,7 @@ describe('ContactAudiences', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.audiences.remove(options), + resend.contacts.segments.remove(options), ).resolves.toMatchInlineSnapshot(` { "data": { @@ -275,8 +275,8 @@ describe('ContactAudiences', () => { const options = {}; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = resend.contacts.audiences.remove( - options as RemoveContactAudiencesOptions, + const result = resend.contacts.segments.remove( + options as RemoveContactSegmentOptions, ); await expect(result).resolves.toMatchInlineSnapshot(` diff --git a/src/contacts/audiences/contact-audiences.ts b/src/contacts/segments/contact-segments.ts similarity index 52% rename from src/contacts/audiences/contact-audiences.ts rename to src/contacts/segments/contact-segments.ts index e743ca10..641d8985 100644 --- a/src/contacts/audiences/contact-audiences.ts +++ b/src/contacts/segments/contact-segments.ts @@ -1,27 +1,27 @@ import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { - AddContactAudiencesOptions, - AddContactAudiencesResponse, - AddContactAudiencesResponseSuccess, -} from './interfaces/add-contact-audience.interface'; + AddContactSegmentOptions, + AddContactSegmentResponse, + AddContactSegmentResponseSuccess, +} from './interfaces/add-contact-segment.interface'; import type { - ListContactAudiencesOptions, - ListContactAudiencesResponse, - ListContactAudiencesResponseSuccess, -} from './interfaces/list-contact-audiences.interface'; + ListContactSegmentsOptions, + ListContactSegmentsResponse, + ListContactSegmentsResponseSuccess, +} from './interfaces/list-contact-segments.interface'; import type { - RemoveContactAudiencesOptions, - RemoveContactAudiencesResponse, - RemoveContactAudiencesResponseSuccess, -} from './interfaces/remove-contact-audience.interface'; + RemoveContactSegmentOptions, + RemoveContactSegmentResponse, + RemoveContactSegmentResponseSuccess, +} from './interfaces/remove-contact-segment.interface'; -export class ContactAudiences { +export class ContactSegments { constructor(private readonly resend: Resend) {} async list( - options: ListContactAudiencesOptions, - ): Promise { + options: ListContactSegmentsOptions, + ): Promise { if (!options.contactId && !options.email) { return { data: null, @@ -36,17 +36,16 @@ export class ContactAudiences { const identifier = options.email ? options.email : options.contactId; const queryString = buildPaginationQuery(options); const url = queryString - ? `/contacts/${identifier}/audiences?${queryString}` - : `/contacts/${identifier}/audiences`; + ? `/contacts/${identifier}/segments?${queryString}` + : `/contacts/${identifier}/segments`; - const data = - await this.resend.get(url); + const data = await this.resend.get(url); return data; } async add( - options: AddContactAudiencesOptions, - ): Promise { + options: AddContactSegmentOptions, + ): Promise { if (!options.contactId && !options.email) { return { data: null, @@ -59,14 +58,14 @@ export class ContactAudiences { } const identifier = options.email ? options.email : options.contactId; - return this.resend.post( - `/contacts/${identifier}/audiences/${options.audienceId}`, + return this.resend.post( + `/contacts/${identifier}/segments/${options.segmentId}`, ); } async remove( - options: RemoveContactAudiencesOptions, - ): Promise { + options: RemoveContactSegmentOptions, + ): Promise { if (!options.contactId && !options.email) { return { data: null, @@ -79,8 +78,8 @@ export class ContactAudiences { } const identifier = options.email ? options.email : options.contactId; - return this.resend.delete( - `/contacts/${identifier}/audiences/${options.audienceId}`, + return this.resend.delete( + `/contacts/${identifier}/segments/${options.segmentId}`, ); } } diff --git a/src/contacts/segments/interfaces/add-contact-segment.interface.ts b/src/contacts/segments/interfaces/add-contact-segment.interface.ts new file mode 100644 index 00000000..ac892f19 --- /dev/null +++ b/src/contacts/segments/interfaces/add-contact-segment.interface.ts @@ -0,0 +1,20 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type AddContactSegmentOptions = ContactSegmentsBaseOptions & { + segmentId: string; +}; + +export interface AddContactSegmentResponseSuccess { + id: string; +} + +export type AddContactSegmentResponse = + | { + data: AddContactSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/audiences/interfaces/contact-audiences.interface.ts b/src/contacts/segments/interfaces/contact-segments.interface.ts similarity index 73% rename from src/contacts/audiences/interfaces/contact-audiences.interface.ts rename to src/contacts/segments/interfaces/contact-segments.interface.ts index 4ffc35cb..cb808201 100644 --- a/src/contacts/audiences/interfaces/contact-audiences.interface.ts +++ b/src/contacts/segments/interfaces/contact-segments.interface.ts @@ -1,4 +1,4 @@ -export type ContactAudiencesBaseOptions = +export type ContactSegmentsBaseOptions = | { contactId: string; email?: never; diff --git a/src/contacts/segments/interfaces/list-contact-segments.interface.ts b/src/contacts/segments/interfaces/list-contact-segments.interface.ts new file mode 100644 index 00000000..2e2b82da --- /dev/null +++ b/src/contacts/segments/interfaces/list-contact-segments.interface.ts @@ -0,0 +1,22 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../../common/interfaces/pagination-options.interface'; +import type { ErrorResponse } from '../../../interfaces'; +import type { Segment } from '../../../segments/interfaces/segment'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type ListContactSegmentsOptions = PaginationOptions & + ContactSegmentsBaseOptions; + +export type ListContactSegmentsResponseSuccess = PaginatedData; + +export type ListContactSegmentsResponse = + | { + data: ListContactSegmentsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/segments/interfaces/remove-contact-segment.interface.ts b/src/contacts/segments/interfaces/remove-contact-segment.interface.ts new file mode 100644 index 00000000..11951347 --- /dev/null +++ b/src/contacts/segments/interfaces/remove-contact-segment.interface.ts @@ -0,0 +1,21 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type RemoveContactSegmentOptions = ContactSegmentsBaseOptions & { + segmentId: string; +}; + +export interface RemoveContactSegmentResponseSuccess { + id: string; + deleted: boolean; +} + +export type RemoveContactSegmentResponse = + | { + data: RemoveContactSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/index.ts b/src/index.ts index 48ae9290..838e4a85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ export * from './api-keys/interfaces'; export * from './attachments/receiving/interfaces'; -export * from './audiences/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; export * from './common/interfaces'; @@ -10,3 +9,4 @@ export * from './emails/interfaces'; export * from './emails/receiving/interfaces'; export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; +export * from './segments/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index 2d2eacd1..23ba683d 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -1,7 +1,6 @@ import { version } from '../package.json'; import { ApiKeys } from './api-keys/api-keys'; import { Attachments } from './attachments/attachments'; -import { Audiences } from './audiences/audiences'; import { Batch } from './batch/batch'; import { Broadcasts } from './broadcasts/broadcasts'; import type { GetOptions, PostOptions, PutOptions } from './common/interfaces'; @@ -11,6 +10,7 @@ import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; import type { ErrorResponse } from './interfaces'; +import { Segments } from './segments/segments'; import { Templates } from './templates/templates'; import { Topics } from './topics/topics'; import { Webhooks } from './webhooks/webhooks'; @@ -31,7 +31,11 @@ export class Resend { readonly apiKeys = new ApiKeys(this); readonly attachments = new Attachments(this); - readonly audiences = new Audiences(this); + readonly segments = new Segments(this); + /** + * @deprecated Use segments instead + */ + readonly audiences = this.segments; readonly batch = new Batch(this); readonly broadcasts = new Broadcasts(this); readonly contacts = new Contacts(this); diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har similarity index 73% rename from src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har index 54fee951..a1645943 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "2a189e724feca29e3c1a8056e5428d06", + "_id": "58407851aec32b12e835bf9b5e7d41cc", "_order": 0, "cache": {}, "request": { - "bodySize": 24, + "bodySize": 23, "cookies": [], "headers": [ { @@ -25,26 +25,26 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { "mimeType": "application/json", "params": [], - "text": "{\"name\":\"Test Audience\"}" + "text": "{\"name\":\"Test Segment\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { - "bodySize": 88, + "bodySize": 86, "content": { "mimeType": "application/json; charset=utf-8", - "size": 88, - "text": "{\"object\":\"audience\",\"id\":\"65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c\",\"name\":\"Test Audience\"}" + "size": 86, + "text": "{\"object\":\"segment\",\"id\":\"3f1e4daf-204e-4b96-bf69-6a567da76e60\",\"name\":\"Test Segment\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b2855c886b495b-SEA" + "value": "992295215f2e7c7d-LAX" }, { "name": "connection", @@ -62,7 +62,7 @@ }, { "name": "content-length", - "value": "88" + "value": "86" }, { "name": "content-type", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:41 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", - "value": "W/\"58-tTHeOTSope7Hsdv56qpKbSlizRE\"" + "value": "W/\"56-qfG19NkGbeZB0nV/O/jZy649j70\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "19" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:40.799Z", - "time": 588, + "startedDateTime": "2025-10-21T17:47:51.700Z", + "time": 323, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 588 + "wait": 323 } }, { - "_id": "5efc9196e5d1c9496521f0439692d11a", + "_id": "bd773a937405a4cd7f10a04d2eaab0a0", "_order": 0, "cache": {}, "request": { @@ -133,21 +133,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c" + "url": "https://api.resend.com/segments/3f1e4daf-204e-4b96-bf69-6a567da76e60" }, "response": { - "bodySize": 80, + "bodySize": 79, "content": { "mimeType": "application/json; charset=utf-8", - "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"65e2f0a3-ed06-4e40-bd3a-5a08c7ca558c\",\"deleted\":true}" + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"3f1e4daf-204e-4b96-bf69-6a567da76e60\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -157,7 +157,7 @@ }, { "name": "cf-ray", - "value": "98b28563bbaa495b-SEA" + "value": "99229522db627c91-LAX" }, { "name": "connection", @@ -173,23 +173,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:42 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", - "value": "W/\"50-ML1zUev5dxBL/DI0b7r5R7dExg0\"" + "value": "W/\"4f-RV03htyYnxmUepbfNF0378P8Gxc\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -204,14 +204,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:41.989Z", - "time": 522, + "startedDateTime": "2025-10-21T17:47:52.026Z", + "time": 302, "timings": { "blocked": -1, "connect": -1, @@ -219,7 +219,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 522 + "wait": 302 } } ], diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har similarity index 84% rename from src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har index 081b4cd9..a48882d7 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "b40ca24e4da48cf9c42eec8d4ee8fd07", + "_id": "69547be0d4508acfcb730bf8e485468b", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,7 +37,7 @@ "text": "{}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { "bodySize": 84, @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b2856adf09495b-SEA" + "value": "99229524798a7c7d-LAX" }, { "name": "connection", @@ -70,7 +70,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:43 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", @@ -78,15 +78,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 422, "statusText": "Unprocessable Entity" }, - "startedDateTime": "2025-10-08T03:23:43.124Z", - "time": 124, + "startedDateTime": "2025-10-21T17:47:52.334Z", + "time": 128, "timings": { "blocked": -1, "connect": -1, @@ -112,7 +112,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 124 + "wait": 128 } } ], diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har similarity index 74% rename from src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har index 5d391fb5..b182855b 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "71e47db40a081d9cea9d924be3674468", + "_id": "d2182e2e249ebf70f05e83b4d9b046e2", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test Audience for Get\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { - "bodySize": 96, + "bodySize": 95, "content": { "mimeType": "application/json; charset=utf-8", - "size": 96, - "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"name\":\"Test Audience for Get\"}" + "size": 95, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"name\":\"Test Audience for Get\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b2856f6abf495b-SEA" + "value": "992295254d517c91-LAX" }, { "name": "connection", @@ -62,7 +62,7 @@ }, { "name": "content-length", - "value": "96" + "value": "95" }, { "name": "content-type", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:44 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", - "value": "W/\"60-2qNyMV2yRuemKADjxL9CSP83RUw\"" + "value": "W/\"5f-eRrNsiyD7BwoBQRhMxHsI6J5rZw\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "16" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:43.857Z", - "time": 192, + "startedDateTime": "2025-10-21T17:47:52.467Z", + "time": 139, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 192 + "wait": 139 } }, { - "_id": "48fa447339cb9fc7aa29a6bc77974fce", + "_id": "09649798d911024796f0e156eae8630e", "_order": 0, "cache": {}, "request": { @@ -133,21 +133,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 218, + "headersSize": 217, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/b9aad85e-2b5a-42ed-bc13-487395888501" + "url": "https://api.resend.com/segments/608d4e2d-f5f3-42a6-8bfb-9da90b6316e9" }, "response": { - "bodySize": 141, + "bodySize": 140, "content": { "mimeType": "application/json; charset=utf-8", - "size": 141, - "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"name\":\"Test Audience for Get\",\"created_at\":\"2025-10-08 03:23:43.967943+00\"}" + "size": 140, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"name\":\"Test Audience for Get\",\"created_at\":\"2025-10-21 17:47:52.582248+00\"}" }, "cookies": [], "headers": [ @@ -157,7 +157,7 @@ }, { "name": "cf-ray", - "value": "98b285745e9e495b-SEA" + "value": "992295262ae67c7d-LAX" }, { "name": "connection", @@ -173,23 +173,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:44 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", - "value": "W/\"8d-SNoapenSoTRhpYIVLTXBnNdI9RA\"" + "value": "W/\"8c-1FQ3+QJKCtjF6djxHI696h01s/c\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "15" }, { "name": "ratelimit-reset", @@ -204,14 +204,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:44.653Z", - "time": 133, + "startedDateTime": "2025-10-21T17:47:52.606Z", + "time": 134, "timings": { "blocked": -1, "connect": -1, @@ -219,11 +219,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 133 + "wait": 134 } }, { - "_id": "15398319a047512af2d8b99cd06f4116", + "_id": "1679cbd7816fde98ed4677fbb020577b", "_order": 0, "cache": {}, "request": { @@ -240,21 +240,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/b9aad85e-2b5a-42ed-bc13-487395888501" + "url": "https://api.resend.com/segments/608d4e2d-f5f3-42a6-8bfb-9da90b6316e9" }, "response": { - "bodySize": 80, + "bodySize": 79, "content": { "mimeType": "application/json; charset=utf-8", - "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"b9aad85e-2b5a-42ed-bc13-487395888501\",\"deleted\":true}" + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -264,7 +264,7 @@ }, { "name": "cf-ray", - "value": "98b28578f9d4495b-SEA" + "value": "992295270ba97c7d-LAX" }, { "name": "connection", @@ -280,23 +280,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:45 GMT" + "value": "Tue, 21 Oct 2025 17:47:52 GMT" }, { "name": "etag", - "value": "W/\"50-vJVsF7PasUJ/5roQJaMWmZEz4Jw\"" + "value": "W/\"4f-AV6E1rZlJDL/ZxqT+ZoqlaES6QI\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "14" }, { "name": "ratelimit-reset", @@ -311,14 +311,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:45.390Z", - "time": 210, + "startedDateTime": "2025-10-21T17:47:52.742Z", + "time": 218, "timings": { "blocked": -1, "connect": -1, @@ -326,7 +326,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 210 + "wait": 218 } } ], diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har similarity index 83% rename from src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har index 4986385f..438e9e74 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "37fe2d2726c58aab7b978ed64c0e5629", + "_id": "ca62098e714a9be90b780b671f4db454", "_order": 0, "cache": {}, "request": { @@ -25,14 +25,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 218, + "headersSize": 217, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/00000000-0000-0000-0000-000000000000" + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" }, "response": { "bodySize": 68, @@ -49,7 +49,7 @@ }, { "name": "cf-ray", - "value": "98b2857e1dcc495b-SEA" + "value": "992295285ce97c7d-LAX" }, { "name": "connection", @@ -65,7 +65,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:46 GMT" + "value": "Tue, 21 Oct 2025 17:47:53 GMT" }, { "name": "etag", @@ -73,15 +73,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "19" }, { "name": "ratelimit-reset", @@ -96,14 +96,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 404, "statusText": "Not Found" }, - "startedDateTime": "2025-10-08T03:23:46.209Z", - "time": 119, + "startedDateTime": "2025-10-21T17:47:52.966Z", + "time": 131, "timings": { "blocked": -1, "connect": -1, @@ -111,7 +111,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 119 + "wait": 131 } } ], diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har similarity index 76% rename from src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har index 378f17ef..da77ae3e 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "acc6807b398db55c7faded600d19fa59", + "_id": "96234cabd1d6945089d83f39f6464b8c", "_order": 0, "cache": {}, "request": { @@ -25,21 +25,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/00000000-0000-0000-0000-000000000000" + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" }, "response": { - "bodySize": 80, + "bodySize": 79, "content": { "mimeType": "application/json; charset=utf-8", - "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -49,7 +49,7 @@ }, { "name": "cf-ray", - "value": "98b28591eedb495b-SEA" + "value": "9922952c5ff07c7d-LAX" }, { "name": "connection", @@ -65,23 +65,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:49 GMT" + "value": "Tue, 21 Oct 2025 17:47:53 GMT" }, { "name": "etag", - "value": "W/\"50-qtTbv74eHSLU1m48Aah48skg91s\"" + "value": "W/\"4f-BAdct1U+nJquUIJWAOwQbWvPxJk\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "15" }, { "name": "ratelimit-reset", @@ -96,14 +96,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:49.377Z", - "time": 306, + "startedDateTime": "2025-10-21T17:47:53.594Z", + "time": 218, "timings": { "blocked": -1, "connect": -1, @@ -111,7 +111,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 306 + "wait": 218 } } ], diff --git a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har similarity index 77% rename from src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har rename to src/segments/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har index 6661c047..b68d0ea8 100644 --- a/src/audiences/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har +++ b/src/segments/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har @@ -8,7 +8,7 @@ }, "entries": [ { - "_id": "69ad88bb02d46c714f3985b02ea225e7", + "_id": "a3b883e60dd1515ce255ac720d46225f", "_order": 0, "cache": {}, "request": { @@ -25,10 +25,10 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 182, + "headersSize": 181, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -37,14 +37,14 @@ "text": "{\"name\":\"Test Audience to Remove\"}" }, "queryString": [], - "url": "https://api.resend.com/audiences" + "url": "https://api.resend.com/segments" }, "response": { - "bodySize": 98, + "bodySize": 97, "content": { "mimeType": "application/json; charset=utf-8", - "size": 98, - "text": "{\"object\":\"audience\",\"id\":\"adee0536-bff3-4d0c-8e8b-aa9c4d7603ad\",\"name\":\"Test Audience to Remove\"}" + "size": 97, + "text": "{\"object\":\"segment\",\"id\":\"fe8ae627-a12c-4c20-bb0f-47b383e51b69\",\"name\":\"Test Audience to Remove\"}" }, "cookies": [], "headers": [ @@ -54,7 +54,7 @@ }, { "name": "cf-ray", - "value": "98b28582a8e3495b-SEA" + "value": "992295293d967c7d-LAX" }, { "name": "connection", @@ -62,7 +62,7 @@ }, { "name": "content-length", - "value": "98" + "value": "97" }, { "name": "content-type", @@ -70,23 +70,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:47 GMT" + "value": "Tue, 21 Oct 2025 17:47:53 GMT" }, { "name": "etag", - "value": "W/\"62-hrZl7qEd/u74uUck14lbdJuEH8Y\"" + "value": "W/\"61-lRIRLIjoHKaMYX9xrsltZaJHeKo\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "18" }, { "name": "ratelimit-reset", @@ -97,14 +97,14 @@ "value": "cloudflare" } ], - "headersSize": 337, + "headersSize": 340, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 201, "statusText": "Created" }, - "startedDateTime": "2025-10-08T03:23:46.937Z", - "time": 123, + "startedDateTime": "2025-10-21T17:47:53.100Z", + "time": 133, "timings": { "blocked": -1, "connect": -1, @@ -112,11 +112,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 123 + "wait": 133 } }, { - "_id": "8d200690d13a16dd7d02225e2fcd6ea8", + "_id": "c484b9f9a5c26dc30ba0c965ebe53486", "_order": 0, "cache": {}, "request": { @@ -133,21 +133,21 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 221, + "headersSize": 220, "httpVersion": "HTTP/1.1", "method": "DELETE", "queryString": [], - "url": "https://api.resend.com/audiences/adee0536-bff3-4d0c-8e8b-aa9c4d7603ad" + "url": "https://api.resend.com/segments/fe8ae627-a12c-4c20-bb0f-47b383e51b69" }, "response": { - "bodySize": 80, + "bodySize": 79, "content": { "mimeType": "application/json; charset=utf-8", - "size": 80, - "text": "{\"object\":\"audience\",\"id\":\"adee0536-bff3-4d0c-8e8b-aa9c4d7603ad\",\"deleted\":true}" + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"fe8ae627-a12c-4c20-bb0f-47b383e51b69\",\"deleted\":true}" }, "cookies": [], "headers": [ @@ -157,7 +157,7 @@ }, { "name": "cf-ray", - "value": "98b285873c22495b-SEA" + "value": "9922952a092c7c91-LAX" }, { "name": "connection", @@ -173,23 +173,23 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:47 GMT" + "value": "Tue, 21 Oct 2025 17:47:53 GMT" }, { "name": "etag", - "value": "W/\"50-RmTgR3D92HzLSC3lrlZGJzC/Ec8\"" + "value": "W/\"4f-pQD64omrG7Z4Y1TKUre8nduwq3U\"" }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "0" + "value": "17" }, { "name": "ratelimit-reset", @@ -204,14 +204,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-10-08T03:23:47.663Z", - "time": 380, + "startedDateTime": "2025-10-21T17:47:53.234Z", + "time": 218, "timings": { "blocked": -1, "connect": -1, @@ -219,11 +219,11 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 380 + "wait": 218 } }, { - "_id": "1fccbbd4bbefafb9777178ba8efaa09b", + "_id": "2e4f579f22a887ce6af131c76729eb07", "_order": 0, "cache": {}, "request": { @@ -240,14 +240,14 @@ }, { "name": "user-agent", - "value": "resend-node:6.2.0-canary.2" + "value": "resend-node:6.3.0-canary.1" } ], - "headersSize": 218, + "headersSize": 217, "httpVersion": "HTTP/1.1", "method": "GET", "queryString": [], - "url": "https://api.resend.com/audiences/adee0536-bff3-4d0c-8e8b-aa9c4d7603ad" + "url": "https://api.resend.com/segments/fe8ae627-a12c-4c20-bb0f-47b383e51b69" }, "response": { "bodySize": 68, @@ -264,7 +264,7 @@ }, { "name": "cf-ray", - "value": "98b2858d4bc0495b-SEA" + "value": "9922952b6f517c7d-LAX" }, { "name": "connection", @@ -280,7 +280,7 @@ }, { "name": "date", - "value": "Wed, 08 Oct 2025 03:23:48 GMT" + "value": "Tue, 21 Oct 2025 17:47:53 GMT" }, { "name": "etag", @@ -288,15 +288,15 @@ }, { "name": "ratelimit-limit", - "value": "2" + "value": "20" }, { "name": "ratelimit-policy", - "value": "2;w=1" + "value": "20;w=1" }, { "name": "ratelimit-remaining", - "value": "1" + "value": "16" }, { "name": "ratelimit-reset", @@ -311,14 +311,14 @@ "value": "chunked" } ], - "headersSize": 367, + "headersSize": 370, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 404, "statusText": "Not Found" }, - "startedDateTime": "2025-10-08T03:23:48.646Z", - "time": 118, + "startedDateTime": "2025-10-21T17:47:53.455Z", + "time": 134, "timings": { "blocked": -1, "connect": -1, @@ -326,7 +326,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 118 + "wait": 134 } } ], diff --git a/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har new file mode 100644 index 00000000..18424c74 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > create > creates a segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "58407851aec32b12e835bf9b5e7d41cc", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 23, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 86, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 86, + "text": "{\"object\":\"segment\",\"id\":\"e87493a1-d7ab-4299-a989-3a22ad5e5b26\",\"name\":\"Test Segment\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922954e09b60feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "86" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"56-qSYXtr2AREWbb+hM4m4hVm/up98\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:58.904Z", + "time": 233, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 233 + } + }, + { + "_id": "327f4f891881b8f6b7fc5ec03753ce0e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/e87493a1-d7ab-4299-a989-3a22ad5e5b26" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"e87493a1-d7ab-4299-a989-3a22ad5e5b26\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922954f4abbb75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-qLdr3x0KjAsapwMZNe4KA39v3pM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:59.140Z", + "time": 599, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 599 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har new file mode 100644 index 00000000..13b28ad9 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har @@ -0,0 +1,122 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > create > handles validation errors", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "69547be0d4508acfcb730bf8e485468b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"statusCode\":422,\"message\":\"Missing `name` field.\",\"name\":\"missing_required_field\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229552cda00feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "84" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"54-b7tWVBvPczzJWDVqTkO4kHnV3MM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-21T17:47:59.744Z", + "time": 134, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 134 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har new file mode 100644 index 00000000..c4b12158 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > get > retrieves a segment by id", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "e35145f1426757232786d3d46a3d19ce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 31, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment for Get\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 94, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 94, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"name\":\"Test Segment for Get\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295539c9db75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "94" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"5e-M9p/d6gj5SqtQN/w8yk2dlkIRjA\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:59.883Z", + "time": 414, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 414 + } + }, + { + "_id": "e595a526a3df2c24c504758eecf76f4d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/1c429758-4d0a-40ca-9a23-c53d3151bc48" + }, + "response": { + "bodySize": 139, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 139, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"name\":\"Test Segment for Get\",\"created_at\":\"2025-10-21 17:48:00.256406+00\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955638990feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"8b-GoBRl3vyVwuAypGOyLm3EXUOIRU\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:00.299Z", + "time": 130, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 130 + } + }, + { + "_id": "36ded9e5fc6246f8668bffb6b40639ce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/1c429758-4d0a-40ca-9a23-c53d3151bc48" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955709730feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-mTt3CfkfwDGk7hS1dpd9eLT7Ya8\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:00.430Z", + "time": 331, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 331 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har new file mode 100644 index 00000000..7360be41 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > get > returns error for non-existent segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "ca62098e714a9be90b780b671f4db454", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295592b6d0feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:48:00.768Z", + "time": 128, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 128 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har new file mode 100644 index 00000000..20419078 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > remove > appears to remove a segment that never existed", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "96234cabd1d6945089d83f39f6464b8c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955d0eb90feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-BAdct1U+nJquUIJWAOwQbWvPxJk\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:01.390Z", + "time": 225, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 225 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har new file mode 100644 index 00000000..926786ff --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > remove > removes a segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "ffd095b200e0c2284c789f801615cb95", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 33, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment to Remove\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 96, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 96, + "text": "{\"object\":\"segment\",\"id\":\"8739500c-d215-46fa-b7fe-c843413bb018\",\"name\":\"Test Segment to Remove\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229559fc250feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "96" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"60-exsu0Qq8e4LfrSvB1/Pqda43928\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:48:00.900Z", + "time": 149, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 149 + } + }, + { + "_id": "4a73d3600c3e436264288a0c729945ae", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/8739500c-d215-46fa-b7fe-c843413bb018" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"8739500c-d215-46fa-b7fe-c843413bb018\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955aee00b75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-oJkvyjPPjL1h+HpXoubDEW58TNo\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "14" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:01.050Z", + "time": 202, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 202 + } + }, + { + "_id": "aeb73a02813509d93917f83332bee7af", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/8739500c-d215-46fa-b7fe-c843413bb018" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955c3e130feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:48:01.254Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/audiences/audiences.integration.spec.ts b/src/segments/audiences.integration.spec.ts similarity index 96% rename from src/audiences/audiences.integration.spec.ts rename to src/segments/audiences.integration.spec.ts index f5b39b66..1c193c51 100644 --- a/src/audiences/audiences.integration.spec.ts +++ b/src/segments/audiences.integration.spec.ts @@ -19,12 +19,12 @@ describe('Audiences Integration Tests', () => { describe('create', () => { it('creates an audience', async () => { const result = await resend.audiences.create({ - name: 'Test Audience', + name: 'Test Segment', }); expect(result.data?.id).toBeTruthy(); expect(result.data?.name).toBeTruthy(); - expect(result.data?.object).toBe('audience'); + expect(result.data?.object).toBe('segment'); const audienceId = result.data!.id; const removeResult = await resend.audiences.remove(audienceId); @@ -107,7 +107,7 @@ describe('Audiences Integration Tests', () => { expect(getResult.data?.id).toBe(audienceId); expect(getResult.data?.name).toBe('Test Audience for Get'); - expect(getResult.data?.object).toBe('audience'); + expect(getResult.data?.object).toBe('segment'); } finally { const removeResult = await resend.audiences.remove(audienceId); expect(removeResult.data?.deleted).toBe(true); diff --git a/src/audiences/audiences.spec.ts b/src/segments/audiences.spec.ts similarity index 86% rename from src/audiences/audiences.spec.ts rename to src/segments/audiences.spec.ts index 8bf771d4..ed076a64 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/segments/audiences.spec.ts @@ -3,12 +3,12 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { - CreateAudienceOptions, - CreateAudienceResponseSuccess, -} from './interfaces/create-audience-options.interface'; -import type { GetAudienceResponseSuccess } from './interfaces/get-audience.interface'; -import type { ListAudiencesResponseSuccess } from './interfaces/list-audiences.interface'; -import type { RemoveAudiencesResponseSuccess } from './interfaces/remove-audience.interface'; + CreateSegmentOptions, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { GetSegmentResponseSuccess } from './interfaces/get-segment.interface'; +import type { ListSegmentsResponseSuccess } from './interfaces/list-segments.interface'; +import type { RemoveSegmentResponseSuccess } from './interfaces/remove-segment.interface'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); @@ -19,11 +19,11 @@ describe('Audiences', () => { describe('create', () => { it('creates a audience', async () => { - const payload: CreateAudienceOptions = { name: 'resend.com' }; - const response: CreateAudienceResponseSuccess = { + const payload: CreateSegmentOptions = { name: 'resend.com' }; + const response: CreateSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', name: 'Resend', - object: 'audience', + object: 'segment', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -42,7 +42,7 @@ describe('Audiences', () => { "data": { "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", "name": "Resend", - "object": "audience", + "object": "segment", }, "error": null, } @@ -50,7 +50,7 @@ describe('Audiences', () => { }); it('throws error when missing name', async () => { - const payload: CreateAudienceOptions = { name: '' }; + const payload: CreateSegmentOptions = { name: '' }; const response: ErrorResponse = { name: 'missing_required_field', message: 'Missing "name" field', @@ -81,7 +81,7 @@ describe('Audiences', () => { }); describe('list', () => { - const response: ListAudiencesResponseSuccess = { + const response: ListSegmentsResponseSuccess = { object: 'list', has_more: false, data: [ @@ -113,7 +113,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences', + 'https://api.resend.com/segments', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -136,7 +136,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1', + 'https://api.resend.com/segments?limit=1', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -160,7 +160,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1&after=cursor-value', + 'https://api.resend.com/segments?limit=1&after=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -184,7 +184,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1&before=cursor-value', + 'https://api.resend.com/segments?limit=1&before=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -227,8 +227,8 @@ describe('Audiences', () => { }); it('get audience', async () => { - const response: GetAudienceResponseSuccess = { - object: 'audience', + const response: GetSegmentResponseSuccess = { + object: 'segment', id: 'fd61172c-cafc-40f5-b049-b45947779a29', name: 'resend.com', created_at: '2023-06-21T06:10:36.144Z', @@ -252,7 +252,7 @@ describe('Audiences', () => { "created_at": "2023-06-21T06:10:36.144Z", "id": "fd61172c-cafc-40f5-b049-b45947779a29", "name": "resend.com", - "object": "audience", + "object": "segment", }, "error": null, } @@ -263,8 +263,8 @@ describe('Audiences', () => { describe('remove', () => { it('removes a audience', async () => { const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; - const response: RemoveAudiencesResponseSuccess = { - object: 'audience', + const response: RemoveSegmentResponseSuccess = { + object: 'segment', id, deleted: true, }; @@ -283,7 +283,7 @@ describe('Audiences', () => { "data": { "deleted": true, "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", - "object": "audience", + "object": "segment", }, "error": null, } diff --git a/src/segments/interfaces/create-segment-options.interface.ts b/src/segments/interfaces/create-segment-options.interface.ts new file mode 100644 index 00000000..da9fa0cc --- /dev/null +++ b/src/segments/interfaces/create-segment-options.interface.ts @@ -0,0 +1,24 @@ +import type { PostOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface CreateSegmentOptions { + name: string; +} + +export interface CreateSegmentRequestOptions extends PostOptions {} + +export interface CreateSegmentResponseSuccess + extends Pick { + object: 'segment'; +} + +export type CreateSegmentResponse = + | { + data: CreateSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/segments/interfaces/get-segment.interface.ts b/src/segments/interfaces/get-segment.interface.ts new file mode 100644 index 00000000..0f22aab2 --- /dev/null +++ b/src/segments/interfaces/get-segment.interface.ts @@ -0,0 +1,17 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface GetSegmentResponseSuccess + extends Pick { + object: 'segment'; +} + +export type GetSegmentResponse = + | { + data: GetSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/segments/interfaces/index.ts b/src/segments/interfaces/index.ts new file mode 100644 index 00000000..c572db5d --- /dev/null +++ b/src/segments/interfaces/index.ts @@ -0,0 +1,5 @@ +export * from './create-segment-options.interface'; +export * from './get-segment.interface'; +export * from './list-segments.interface'; +export * from './remove-segment.interface'; +export * from './segment'; diff --git a/src/audiences/interfaces/list-audiences.interface.ts b/src/segments/interfaces/list-segments.interface.ts similarity index 51% rename from src/audiences/interfaces/list-audiences.interface.ts rename to src/segments/interfaces/list-segments.interface.ts index 54727fea..17f9e99a 100644 --- a/src/audiences/interfaces/list-audiences.interface.ts +++ b/src/segments/interfaces/list-segments.interface.ts @@ -1,18 +1,18 @@ import type { PaginationOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; +import type { Segment } from './segment'; -export type ListAudiencesOptions = PaginationOptions; +export type ListSegmentsOptions = PaginationOptions; -export type ListAudiencesResponseSuccess = { +export type ListSegmentsResponseSuccess = { object: 'list'; - data: Audience[]; + data: Segment[]; has_more: boolean; }; -export type ListAudiencesResponse = +export type ListSegmentsResponse = | { - data: ListAudiencesResponseSuccess; + data: ListSegmentsResponseSuccess; error: null; } | { diff --git a/src/segments/interfaces/remove-segment.interface.ts b/src/segments/interfaces/remove-segment.interface.ts new file mode 100644 index 00000000..887ded12 --- /dev/null +++ b/src/segments/interfaces/remove-segment.interface.ts @@ -0,0 +1,17 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface RemoveSegmentResponseSuccess extends Pick { + object: 'segment'; + deleted: boolean; +} + +export type RemoveSegmentResponse = + | { + data: RemoveSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/interfaces/audience.ts b/src/segments/interfaces/segment.ts similarity index 65% rename from src/audiences/interfaces/audience.ts rename to src/segments/interfaces/segment.ts index 7335960d..08a014a3 100644 --- a/src/audiences/interfaces/audience.ts +++ b/src/segments/interfaces/segment.ts @@ -1,4 +1,4 @@ -export interface Audience { +export interface Segment { created_at: string; id: string; name: string; diff --git a/src/segments/segments.integration.spec.ts b/src/segments/segments.integration.spec.ts new file mode 100644 index 00000000..7fbe0f73 --- /dev/null +++ b/src/segments/segments.integration.spec.ts @@ -0,0 +1,151 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ +import type { Polly } from '@pollyjs/core'; +import { Resend } from '../resend'; +import { setupPolly } from '../test-utils/polly-setup'; + +describe('Segments Integration Tests', () => { + let polly: Polly; + let resend: Resend; + + beforeEach(() => { + polly = setupPolly(); + resend = new Resend(process.env.RESEND_API_KEY || 're_fake_key'); + }); + + afterEach(async () => { + await polly.stop(); + }); + + describe('create', () => { + it('creates a segment', async () => { + const result = await resend.segments.create({ + name: 'Test Segment', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.name).toBeTruthy(); + expect(result.data?.object).toBe('segment'); + const segmentId = result.data!.id; + + const removeResult = await resend.segments.remove(segmentId); + expect(removeResult.data?.deleted).toBe(true); + }); + + it('handles validation errors', async () => { + // @ts-expect-error: Testing invalid input + const result = await resend.segments.create({}); + + expect(result.error?.name).toBe('missing_required_field'); + }); + }); + + // Needs to be run with an account that can have multiple segments + describe.todo('list', () => { + it('lists segments without pagination', async () => { + const segmentIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.segments.create({ + name: `Test segment ${i} for listing`, + }); + + expect(createResult.data?.id).toBeTruthy(); + segmentIds.push(createResult.data!.id); + } + + const result = await resend.segments.list(); + + expect(result.data?.object).toBe('list'); + expect(result.data?.data.length).toBeGreaterThanOrEqual(6); + expect(result.data?.has_more).toBe(false); + } finally { + for (const id of segmentIds) { + const removeResult = await resend.segments.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + + it('lists segments with limit', async () => { + const segmentIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.segments.create({ + name: `Test segment ${i} for listing with limit`, + }); + + expect(createResult.data?.id).toBeTruthy(); + segmentIds.push(createResult.data!.id); + } + + const result = await resend.segments.list({ limit: 5 }); + + expect(result.data?.data.length).toBe(5); + expect(result.data?.has_more).toBe(true); + } finally { + for (const id of segmentIds) { + const removeResult = await resend.segments.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + }); + + describe('get', () => { + it('retrieves a segment by id', async () => { + const createResult = await resend.segments.create({ + name: 'Test Segment for Get', + }); + + expect(createResult.data?.id).toBeTruthy(); + const segmentId = createResult.data!.id; + + try { + const getResult = await resend.segments.get(segmentId); + + expect(getResult.data?.id).toBe(segmentId); + expect(getResult.data?.name).toBe('Test Segment for Get'); + expect(getResult.data?.object).toBe('segment'); + } finally { + const removeResult = await resend.segments.remove(segmentId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('returns error for non-existent segment', async () => { + const result = await resend.segments.get( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.error?.name).toBe('not_found'); + }); + }); + + describe('remove', () => { + it('removes a segment', async () => { + const createResult = await resend.segments.create({ + name: 'Test Segment to Remove', + }); + + expect(createResult.data?.id).toBeTruthy(); + const segmentId = createResult.data!.id; + + const removeResult = await resend.segments.remove(segmentId); + + expect(removeResult.data?.deleted).toBe(true); + + const getResult = await resend.segments.get(segmentId); + expect(getResult.error?.name).toBe('not_found'); + }); + + it('appears to remove a segment that never existed', async () => { + const result = await resend.segments.remove( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.data?.deleted).toBe(true); + }); + }); +}); diff --git a/src/segments/segments.spec.ts b/src/segments/segments.spec.ts new file mode 100644 index 00000000..92d79f0f --- /dev/null +++ b/src/segments/segments.spec.ts @@ -0,0 +1,291 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { + CreateSegmentOptions, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { GetSegmentResponseSuccess } from './interfaces/get-segment.interface'; +import type { ListSegmentsResponseSuccess } from './interfaces/list-segments.interface'; +import type { RemoveSegmentResponseSuccess } from './interfaces/remove-segment.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('Segments', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a segment', async () => { + const payload: CreateSegmentOptions = { name: 'resend.com' }; + const response: CreateSegmentResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + name: 'Resend', + object: 'segment', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.segments.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", + "name": "Resend", + "object": "segment", + }, + "error": null, +} +`); + }); + + it('throws error when missing name', async () => { + const payload: CreateSegmentOptions = { name: '' }; + const response: ErrorResponse = { + name: 'missing_required_field', + message: 'Missing "name" field', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.segments.create(payload); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing "name" field", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('list', () => { + const response: ListSegmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + name: 'resend.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + name: 'react.email', + created_at: '2023-04-07T23:13:20.417116+00:00', + }, + ], + }; + + describe('when no pagination options are provided', () => { + it('lists audiences', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.segments.list(); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('when pagination options are provided', () => { + it('passes limit param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ limit: 1 }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes after param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ + limit: 1, + after: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1&after=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes before param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ + limit: 1, + before: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1&before=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + }); + + describe('get', () => { + describe('when audience not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Audience not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.segments.get('1234'); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Audience not found", + "name": "not_found", + }, +} +`); + }); + }); + + it('get audience', async () => { + const response: GetSegmentResponseSuccess = { + object: 'segment', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'resend.com', + created_at: '2023-06-21T06:10:36.144Z', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.segments.get('1234')).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2023-06-21T06:10:36.144Z", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "resend.com", + "object": "segment", + }, + "error": null, +} +`); + }); + }); + + describe('remove', () => { + it('removes a audience', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response: RemoveSegmentResponseSuccess = { + object: 'segment', + id, + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.segments.remove(id)).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", + "object": "segment", + }, + "error": null, +} +`); + }); + }); +}); diff --git a/src/segments/segments.ts b/src/segments/segments.ts new file mode 100644 index 00000000..5db4fce6 --- /dev/null +++ b/src/segments/segments.ts @@ -0,0 +1,59 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + CreateSegmentOptions, + CreateSegmentRequestOptions, + CreateSegmentResponse, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { + GetSegmentResponse, + GetSegmentResponseSuccess, +} from './interfaces/get-segment.interface'; +import type { + ListSegmentsOptions, + ListSegmentsResponse, + ListSegmentsResponseSuccess, +} from './interfaces/list-segments.interface'; +import type { + RemoveSegmentResponse, + RemoveSegmentResponseSuccess, +} from './interfaces/remove-segment.interface'; + +export class Segments { + constructor(private readonly resend: Resend) {} + + async create( + payload: CreateSegmentOptions, + options: CreateSegmentRequestOptions = {}, + ): Promise { + const data = await this.resend.post( + '/segments', + payload, + options, + ); + return data; + } + + async list(options: ListSegmentsOptions = {}): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/segments?${queryString}` : '/segments'; + + const data = await this.resend.get(url); + return data; + } + + async get(id: string): Promise { + const data = await this.resend.get( + `/segments/${id}`, + ); + return data; + } + + async remove(id: string): Promise { + const data = await this.resend.delete( + `/segments/${id}`, + ); + return data; + } +} From f2078757684910acec987c8e0ebdcb9fec15625b Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Tue, 21 Oct 2025 16:05:47 -0300 Subject: [PATCH 34/49] feat: add message_id to inbound email response (#702) --- src/emails/receiving/interfaces/get-inbound-email.interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emails/receiving/interfaces/get-inbound-email.interface.ts b/src/emails/receiving/interfaces/get-inbound-email.interface.ts index 4e19c00d..ffbb32a7 100644 --- a/src/emails/receiving/interfaces/get-inbound-email.interface.ts +++ b/src/emails/receiving/interfaces/get-inbound-email.interface.ts @@ -13,6 +13,7 @@ export interface GetInboundEmailResponseSuccess { html: string | null; text: string | null; headers: Record; + message_id: string; attachments: Array<{ id: string; filename: string; From eba02883d496e9e382457f739d994373134c3c04 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Tue, 21 Oct 2025 16:05:50 -0300 Subject: [PATCH 35/49] feat: add size to attachment response (#701) --- src/attachments/receiving/interfaces/attachment.ts | 1 + .../receiving/interfaces/list-attachments.interface.ts | 10 +--------- .../interfaces/get-inbound-email.interface.ts | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts index e04179ab..3414aca7 100644 --- a/src/attachments/receiving/interfaces/attachment.ts +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -1,6 +1,7 @@ export interface InboundAttachment { id: string; filename?: string; + size: number; content_type: string; content_disposition: 'inline' | 'attachment'; content_id?: string; diff --git a/src/attachments/receiving/interfaces/list-attachments.interface.ts b/src/attachments/receiving/interfaces/list-attachments.interface.ts index 3f174645..94c36dca 100644 --- a/src/attachments/receiving/interfaces/list-attachments.interface.ts +++ b/src/attachments/receiving/interfaces/list-attachments.interface.ts @@ -9,15 +9,7 @@ export type ListAttachmentsOptions = PaginationOptions & { export interface ListAttachmentsApiResponse { object: 'list'; has_more: boolean; - data: Array<{ - id: string; - filename?: string; - content_type: string; - content_disposition: 'inline' | 'attachment'; - content_id?: string; - download_url: string; - expires_at: string; - }>; + data: InboundAttachment[]; } export interface ListAttachmentsResponseSuccess { diff --git a/src/emails/receiving/interfaces/get-inbound-email.interface.ts b/src/emails/receiving/interfaces/get-inbound-email.interface.ts index ffbb32a7..b8d7ed49 100644 --- a/src/emails/receiving/interfaces/get-inbound-email.interface.ts +++ b/src/emails/receiving/interfaces/get-inbound-email.interface.ts @@ -17,6 +17,7 @@ export interface GetInboundEmailResponseSuccess { attachments: Array<{ id: string; filename: string; + size: number; content_type: string; content_id: string; content_disposition: string; From 06e0abe52a1f86112031e70297b369ee25d04609 Mon Sep 17 00:00:00 2001 From: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:39:44 -0300 Subject: [PATCH 36/49] fix: create topics default subscription (#703) --- src/topics/topics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/topics/topics.ts b/src/topics/topics.ts index 46d7a16c..95353f09 100644 --- a/src/topics/topics.ts +++ b/src/topics/topics.ts @@ -30,7 +30,7 @@ export class Topics { const data = await this.resend.post('/topics', { ...body, - defaultSubscription: defaultSubscription, + default_subscription: defaultSubscription, }); return data; From d28a249819b9fe46bb813bdb9e51dc53b91c32d0 Mon Sep 17 00:00:00 2001 From: Alexandre Cisneiros Date: Tue, 21 Oct 2025 13:31:12 -0700 Subject: [PATCH 37/49] fix: contact topics list should return a normal paginated list (#704) --- src/contacts/topics/contact-topics.spec.ts | 124 ++++++++---------- src/contacts/topics/contact-topics.ts | 16 +-- ...ce.ts => list-contact-topics.interface.ts} | 18 +-- 3 files changed, 70 insertions(+), 88 deletions(-) rename src/contacts/topics/interfaces/{get-contact-topics.interface.ts => list-contact-topics.interface.ts} (55%) diff --git a/src/contacts/topics/contact-topics.spec.ts b/src/contacts/topics/contact-topics.spec.ts index 20a9f875..8188dbf8 100644 --- a/src/contacts/topics/contact-topics.spec.ts +++ b/src/contacts/topics/contact-topics.spec.ts @@ -2,9 +2,9 @@ import createFetchMock from 'vitest-fetch-mock'; import { Resend } from '../../resend'; import { mockSuccessResponse } from '../../test-utils/mock-fetch'; import type { - GetContactTopicsOptions, - GetContactTopicsResponseSuccess, -} from './interfaces/get-contact-topics.interface'; + ListContactTopicsOptions, + ListContactTopicsResponseSuccess, +} from './interfaces/list-contact-topics.interface'; import type { UpdateContactTopicsOptions, UpdateContactTopicsResponseSuccess, @@ -174,31 +174,28 @@ describe('ContactTopics', () => { }); }); - describe('get', () => { + describe('list', () => { it('gets contact topics by email', async () => { - const options: GetContactTopicsOptions = { + const options: ListContactTopicsOptions = { email: 'carolina@resend.com', }; - const response: GetContactTopicsResponseSuccess = { + const response: ListContactTopicsResponseSuccess = { has_more: false, object: 'list', - data: { - email: 'carolina@resend.com', - topics: [ - { - id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', - name: 'Test Topic', - description: 'This is a test topic', - subscription: 'opt_in', - }, - { - id: 'another-topic-id', - name: 'Another Topic', - description: null, - subscription: 'opt_out', - }, - ], - }, + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Topic', + description: 'This is a test topic', + subscription: 'opt_in', + }, + { + id: 'another-topic-id', + name: 'Another Topic', + description: null, + subscription: 'opt_out', + }, + ], }; mockSuccessResponse(response, { @@ -207,27 +204,24 @@ describe('ContactTopics', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.topics.get(options), + resend.contacts.topics.list(options), ).resolves.toMatchInlineSnapshot(` { "data": { - "data": { - "email": "carolina@resend.com", - "topics": [ - { - "description": "This is a test topic", - "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", - "name": "Test Topic", - "subscription": "opt_in", - }, - { - "description": null, - "id": "another-topic-id", - "name": "Another Topic", - "subscription": "opt_out", - }, - ], - }, + "data": [ + { + "description": "This is a test topic", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Topic", + "subscription": "opt_in", + }, + { + "description": null, + "id": "another-topic-id", + "name": "Another Topic", + "subscription": "opt_out", + }, + ], "has_more": false, "object": "list", }, @@ -237,23 +231,20 @@ describe('ContactTopics', () => { }); it('gets contact topics by ID', async () => { - const options: GetContactTopicsOptions = { + const options: ListContactTopicsOptions = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', }; - const response: GetContactTopicsResponseSuccess = { + const response: ListContactTopicsResponseSuccess = { has_more: false, object: 'list', - data: { - email: 'carolina@resend.com', - topics: [ - { - id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', - name: 'Test Topic', - description: 'This is a test topic', - subscription: 'opt_in', - }, - ], - }, + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Topic', + description: 'This is a test topic', + subscription: 'opt_in', + }, + ], }; mockSuccessResponse(response, { @@ -262,21 +253,18 @@ describe('ContactTopics', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); await expect( - resend.contacts.topics.get(options), + resend.contacts.topics.list(options), ).resolves.toMatchInlineSnapshot(` { "data": { - "data": { - "email": "carolina@resend.com", - "topics": [ - { - "description": "This is a test topic", - "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", - "name": "Test Topic", - "subscription": "opt_in", - }, - ], - }, + "data": [ + { + "description": "This is a test topic", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Topic", + "subscription": "opt_in", + }, + ], "has_more": false, "object": "list", }, @@ -289,8 +277,8 @@ describe('ContactTopics', () => { const options = {}; const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = resend.contacts.topics.get( - options as GetContactTopicsOptions, + const result = resend.contacts.topics.list( + options as ListContactTopicsOptions, ); await expect(result).resolves.toMatchInlineSnapshot(` diff --git a/src/contacts/topics/contact-topics.ts b/src/contacts/topics/contact-topics.ts index e87bd4a4..3cc12810 100644 --- a/src/contacts/topics/contact-topics.ts +++ b/src/contacts/topics/contact-topics.ts @@ -1,10 +1,10 @@ import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; import type { Resend } from '../../resend'; import type { - GetContactTopicsOptions, - GetContactTopicsResponse, - GetContactTopicsResponseSuccess, -} from './interfaces/get-contact-topics.interface'; + ListContactTopicsOptions, + ListContactTopicsResponse, + ListContactTopicsResponseSuccess, +} from './interfaces/list-contact-topics.interface'; import type { UpdateContactTopicsOptions, UpdateContactTopicsResponse, @@ -37,9 +37,9 @@ export class ContactTopics { return data; } - async get( - options: GetContactTopicsOptions, - ): Promise { + async list( + options: ListContactTopicsOptions, + ): Promise { if (!options.id && !options.email) { return { data: null, @@ -57,7 +57,7 @@ export class ContactTopics { ? `/contacts/${identifier}/topics?${queryString}` : `/contacts/${identifier}/topics`; - const data = await this.resend.get(url); + const data = await this.resend.get(url); return data; } } diff --git a/src/contacts/topics/interfaces/get-contact-topics.interface.ts b/src/contacts/topics/interfaces/list-contact-topics.interface.ts similarity index 55% rename from src/contacts/topics/interfaces/get-contact-topics.interface.ts rename to src/contacts/topics/interfaces/list-contact-topics.interface.ts index 5a0c4e29..b4aaa795 100644 --- a/src/contacts/topics/interfaces/get-contact-topics.interface.ts +++ b/src/contacts/topics/interfaces/list-contact-topics.interface.ts @@ -5,15 +5,15 @@ import type { } from '../../../common/interfaces/pagination-options.interface'; import type { ErrorResponse } from '../../../interfaces'; -interface GetContactTopicsBaseOptions { +interface ListContactTopicsBaseOptions { id?: string; email?: string; } -export type GetContactTopicsOptions = GetContactTopicsBaseOptions & +export type ListContactTopicsOptions = ListContactTopicsBaseOptions & PaginationOptions; -export interface GetContactTopicsRequestOptions extends GetOptions {} +export interface ListContactTopicsRequestOptions extends GetOptions {} export interface ContactTopic { id: string; @@ -22,17 +22,11 @@ export interface ContactTopic { subscription: 'opt_in' | 'opt_out'; } -export type GetContactTopicsResponseSuccess = PaginatedData<{ - email: string; - topics: ContactTopic[]; -}>; +export type ListContactTopicsResponseSuccess = PaginatedData; -export type GetContactTopicsResponse = +export type ListContactTopicsResponse = | { - data: PaginatedData<{ - email: string; - topics: ContactTopic[]; - }>; + data: ListContactTopicsResponseSuccess; error: null; } | { From 71c14ad3ff3dd1c3098e6cfa3b71201147072d55 Mon Sep 17 00:00:00 2001 From: Alexandre Cisneiros Date: Wed, 22 Oct 2025 08:43:43 -0700 Subject: [PATCH 38/49] feat: add contact properties (#705) --- ...parse-contact-properties-to-api-options.ts | 40 +++ .../contact-properties.spec.ts | 331 ++++++++++++++++++ src/contact-properties/contact-properties.ts | 134 +++++++ .../interfaces/contact-property.ts | 35 ++ ...eate-contact-property-options.interface.ts | 34 ++ ...lete-contact-property-options.interface.ts | 14 + .../get-contact-property.interface.ts | 14 + ...st-contact-properties-options.interface.ts | 25 ++ ...date-contact-property-options.interface.ts | 23 ++ src/resend.ts | 2 + 10 files changed, 652 insertions(+) create mode 100644 src/common/utils/parse-contact-properties-to-api-options.ts create mode 100644 src/contact-properties/contact-properties.spec.ts create mode 100644 src/contact-properties/contact-properties.ts create mode 100644 src/contact-properties/interfaces/contact-property.ts create mode 100644 src/contact-properties/interfaces/create-contact-property-options.interface.ts create mode 100644 src/contact-properties/interfaces/delete-contact-property-options.interface.ts create mode 100644 src/contact-properties/interfaces/get-contact-property.interface.ts create mode 100644 src/contact-properties/interfaces/list-contact-properties-options.interface.ts create mode 100644 src/contact-properties/interfaces/update-contact-property-options.interface.ts diff --git a/src/common/utils/parse-contact-properties-to-api-options.ts b/src/common/utils/parse-contact-properties-to-api-options.ts new file mode 100644 index 00000000..f9a276ea --- /dev/null +++ b/src/common/utils/parse-contact-properties-to-api-options.ts @@ -0,0 +1,40 @@ +import type { + ApiContactProperty, + ContactProperty, +} from '../../contact-properties/interfaces/contact-property'; +import type { + CreateContactPropertyApiOptions, + CreateContactPropertyOptions, +} from '../../contact-properties/interfaces/create-contact-property-options.interface'; +import type { + UpdateContactPropertyApiOptions, + UpdateContactPropertyOptions, +} from '../../contact-properties/interfaces/update-contact-property-options.interface'; + +export function parseContactPropertyFromApi( + contactProperty: ApiContactProperty, +): ContactProperty { + return { + id: contactProperty.id, + key: contactProperty.key, + object: contactProperty.object, + createdAt: contactProperty.created_at, + type: contactProperty.type, + fallbackValue: contactProperty.fallback_value, + } as ContactProperty; +} + +export function parseContactPropertyToApiOptions( + contactProperty: CreateContactPropertyOptions | UpdateContactPropertyOptions, +): CreateContactPropertyApiOptions | UpdateContactPropertyApiOptions { + if ('key' in contactProperty) { + return { + key: contactProperty.key, + type: contactProperty.type, + fallback_value: contactProperty.fallbackValue, + }; + } + return { + fallback_value: contactProperty.fallbackValue, + }; +} diff --git a/src/contact-properties/contact-properties.spec.ts b/src/contact-properties/contact-properties.spec.ts new file mode 100644 index 00000000..c124cd3d --- /dev/null +++ b/src/contact-properties/contact-properties.spec.ts @@ -0,0 +1,331 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + CreateContactPropertyOptions, + CreateContactPropertyResponseSuccess, +} from './interfaces/create-contact-property-options.interface'; +import type { RemoveContactPropertyResponseSuccess } from './interfaces/delete-contact-property-options.interface'; +import type { GetContactPropertyResponseSuccess } from './interfaces/get-contact-property.interface'; +import type { ListContactPropertiesResponseSuccess } from './interfaces/list-contact-properties-options.interface'; +import type { + UpdateContactPropertyOptions, + UpdateContactPropertyResponseSuccess, +} from './interfaces/update-contact-property-options.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('ContactProperties', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a contact property', async () => { + const payload: CreateContactPropertyOptions = { + key: 'country', + type: 'string', + fallbackValue: 'unknown', + }; + const response: CreateContactPropertyResponseSuccess = { + object: 'contact_property', + id: '3deaccfb-f47f-440a-8875-ea14b1716b43', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3deaccfb-f47f-440a-8875-ea14b1716b43", + "object": "contact_property", + }, + "error": null, +} +`); + }); + + it('throws error when missing key', async () => { + // @ts-expect-error - Testing invalid input + const payload: CreateContactPropertyOptions = { + type: 'string', + fallbackValue: 'unknown', + }; + const response: ErrorResponse = { + statusCode: 422, + name: 'missing_required_field', + message: 'Missing `key` field.', + }; + + mockErrorResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`key\` field.", + "name": "missing_required_field", + "statusCode": 422, + }, +} +`); + }); + }); + + describe('list', () => { + it('lists contact properties', async () => { + const response: ListContactPropertiesResponseSuccess = { + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + key: 'country', + type: 'string', + fallback_value: 'unknown', + object: 'contact_property', + created_at: '2021-01-01T00:00:00.000Z', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + key: 'edition', + type: 'number', + fallback_value: 1, + object: 'contact_property', + created_at: '2021-01-01T00:00:00.000Z', + }, + ], + object: 'list', + has_more: false, + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.list(), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "createdAt": "2021-01-01T00:00:00.000Z", + "fallbackValue": "unknown", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "key": "country", + "object": "contact_property", + "type": "string", + }, + { + "createdAt": "2021-01-01T00:00:00.000Z", + "fallbackValue": 1, + "id": "ac7503ac-e027-4aea-94b3-b0acd46f65f9", + "key": "edition", + "object": "contact_property", + "type": "number", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + }); + + describe('get', () => { + it('gets a contact property by id', async () => { + const response: GetContactPropertyResponseSuccess = { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + key: 'country', + type: 'string', + fallback_value: 'unknown', + object: 'contact_property', + created_at: '2021-01-01T00:00:00.000Z', + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.get('b6d24b8e-af0b-4c3c-be0c-359bbd97381e'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "createdAt": "2021-01-01T00:00:00.000Z", + "fallbackValue": "unknown", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "key": "country", + "object": "contact_property", + "type": "string", + }, + "error": null, +} +`); + }); + + it('returns error when missing id', async () => { + const response: ErrorResponse = { + statusCode: null, + name: 'missing_required_field', + message: 'Missing `id` field.', + }; + mockErrorResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.get(''), + ).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + + it('returns error when contact property not found', async () => { + const response: ErrorResponse = { + statusCode: 404, + name: 'not_found', + message: 'Contact property not found', + }; + mockErrorResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.get('b6d24b8e-af0b-4c3c-be0c-359bbd97381e'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Contact property not found", + "name": "not_found", + "statusCode": 404, + }, +} +`); + }); + }); + + describe('update', () => { + it('updates a contact property', async () => { + const payload: UpdateContactPropertyOptions = { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + fallbackValue: 'new value', + }; + const response: UpdateContactPropertyResponseSuccess = { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + object: 'contact_property', + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "object": "contact_property", + }, + "error": null, +} +`); + }); + it('returns error when missing id', async () => { + const payload: UpdateContactPropertyOptions = { + id: '', + fallbackValue: 'new value', + }; + const response: ErrorResponse = { + statusCode: null, + name: 'missing_required_field', + message: 'Missing `id` field.', + }; + mockErrorResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.update(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + }); + + describe('remove', () => { + it('removes a contact property', async () => { + const response: RemoveContactPropertyResponseSuccess = { + deleted: true, + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + object: 'contact_property', + }; + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.remove('b6d24b8e-af0b-4c3c-be0c-359bbd97381e'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "object": "contact_property", + }, + "error": null, +} +`); + }); + + it('returns error when missing id', async () => { + const response: ErrorResponse = { + statusCode: null, + name: 'missing_required_field', + message: 'Missing `id` field.', + }; + mockErrorResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contactProperties.remove(''), + ).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + }); +}); diff --git a/src/contact-properties/contact-properties.ts b/src/contact-properties/contact-properties.ts new file mode 100644 index 00000000..8dc032ec --- /dev/null +++ b/src/contact-properties/contact-properties.ts @@ -0,0 +1,134 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import { + parseContactPropertyFromApi, + parseContactPropertyToApiOptions, +} from '../common/utils/parse-contact-properties-to-api-options'; +import type { Resend } from '../resend'; +import type { + CreateContactPropertyOptions, + CreateContactPropertyResponse, + CreateContactPropertyResponseSuccess, +} from './interfaces/create-contact-property-options.interface'; +import type { + RemoveContactPropertyResponse, + RemoveContactPropertyResponseSuccess, +} from './interfaces/delete-contact-property-options.interface'; +import type { + GetContactPropertyResponse, + GetContactPropertyResponseSuccess, +} from './interfaces/get-contact-property.interface'; +import type { + ListContactPropertiesOptions, + ListContactPropertiesResponse, + ListContactPropertiesResponseSuccess, +} from './interfaces/list-contact-properties-options.interface'; +import type { + UpdateContactPropertyOptions, + UpdateContactPropertyResponse, + UpdateContactPropertyResponseSuccess, +} from './interfaces/update-contact-property-options.interface'; + +export class ContactProperties { + constructor(private readonly resend: Resend) {} + + async create( + options: CreateContactPropertyOptions, + ): Promise { + const apiOptions = parseContactPropertyToApiOptions(options); + const data = await this.resend.post( + '/contact-properties', + apiOptions, + ); + return data; + } + + async list( + options: ListContactPropertiesOptions = {}, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/contact-properties?${queryString}` + : '/contact-properties'; + + const response = + await this.resend.get(url); + + if (response.data) { + return { + data: { + ...response.data, + data: response.data.data.map((apiContactProperty) => + parseContactPropertyFromApi(apiContactProperty), + ), + }, + error: null, + }; + } + + return response; + } + + async get(id: string): Promise { + if (!id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + const response = await this.resend.get( + `/contact-properties/${id}`, + ); + + if (response.data) { + return { + data: parseContactPropertyFromApi(response.data), + error: null, + }; + } + + return response; + } + + async update( + payload: UpdateContactPropertyOptions, + ): Promise { + if (!payload.id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + + const apiOptions = parseContactPropertyToApiOptions(payload); + const data = await this.resend.patch( + `/contact-properties/${payload.id}`, + apiOptions, + ); + return data; + } + + async remove(id: string): Promise { + if (!id) { + return { + data: null, + error: { + message: 'Missing `id` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + const data = await this.resend.delete( + `/contact-properties/${id}`, + ); + return data; + } +} diff --git a/src/contact-properties/interfaces/contact-property.ts b/src/contact-properties/interfaces/contact-property.ts new file mode 100644 index 00000000..cc018ac5 --- /dev/null +++ b/src/contact-properties/interfaces/contact-property.ts @@ -0,0 +1,35 @@ +// API types +type StringBaseApiContactProperty = { + type: 'string'; + fallback_value: string | null; +}; + +type NumberBaseApiContactProperty = { + type: 'number'; + fallback_value: number | null; +}; + +export type ApiContactProperty = { + id: string; + object: 'contact_property'; + created_at: string; + key: string; +} & (StringBaseApiContactProperty | NumberBaseApiContactProperty); + +// SDK types +type StringBaseContactProperty = { + type: 'string'; + fallbackValue: string | null; +}; + +type NumberBaseContactProperty = { + type: 'number'; + fallbackValue: number | null; +}; + +export type ContactProperty = { + id: string; + object: 'contact_property'; + createdAt: string; + key: string; +} & (StringBaseContactProperty | NumberBaseContactProperty); diff --git a/src/contact-properties/interfaces/create-contact-property-options.interface.ts b/src/contact-properties/interfaces/create-contact-property-options.interface.ts new file mode 100644 index 00000000..c36651a5 --- /dev/null +++ b/src/contact-properties/interfaces/create-contact-property-options.interface.ts @@ -0,0 +1,34 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { ContactProperty } from './contact-property'; + +// SDK options +type StringBaseContactPropertyOptions = { + type: 'string'; + fallbackValue?: string | null; +}; + +type NumberBaseContactPropertyOptions = { + type: 'number'; + fallbackValue?: number | null; +}; + +export type CreateContactPropertyOptions = { + key: string; +} & (StringBaseContactPropertyOptions | NumberBaseContactPropertyOptions); + +// API options +export interface CreateContactPropertyApiOptions { + key: string; + type: 'string' | 'number'; + fallback_value?: string | number | null; +} + +export type CreateContactPropertyResponseSuccess = Pick< + ContactProperty, + 'id' | 'object' +>; + +export interface CreateContactPropertyResponse { + data: CreateContactPropertyResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/contact-properties/interfaces/delete-contact-property-options.interface.ts b/src/contact-properties/interfaces/delete-contact-property-options.interface.ts new file mode 100644 index 00000000..784987dd --- /dev/null +++ b/src/contact-properties/interfaces/delete-contact-property-options.interface.ts @@ -0,0 +1,14 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { ContactProperty } from './contact-property'; + +export type RemoveContactPropertyResponseSuccess = Pick< + ContactProperty, + 'id' | 'object' +> & { + deleted: boolean; +}; + +export interface RemoveContactPropertyResponse { + data: RemoveContactPropertyResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/contact-properties/interfaces/get-contact-property.interface.ts b/src/contact-properties/interfaces/get-contact-property.interface.ts new file mode 100644 index 00000000..7d37a474 --- /dev/null +++ b/src/contact-properties/interfaces/get-contact-property.interface.ts @@ -0,0 +1,14 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { ApiContactProperty, ContactProperty } from './contact-property'; + +export type GetContactPropertyResponseSuccess = ApiContactProperty; + +export type GetContactPropertyResponse = + | { + data: ContactProperty; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contact-properties/interfaces/list-contact-properties-options.interface.ts b/src/contact-properties/interfaces/list-contact-properties-options.interface.ts new file mode 100644 index 00000000..20377fbc --- /dev/null +++ b/src/contact-properties/interfaces/list-contact-properties-options.interface.ts @@ -0,0 +1,25 @@ +import type { PaginationOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; +import type { ApiContactProperty, ContactProperty } from './contact-property'; + +export type ListContactPropertiesOptions = PaginationOptions; + +export type ListContactPropertiesResponseSuccess = { + object: 'list'; + has_more: boolean; + data: ApiContactProperty[]; +}; + +export type ListContactPropertiesResponse = + | { + data: { + object: 'list'; + has_more: boolean; + data: ContactProperty[]; + }; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contact-properties/interfaces/update-contact-property-options.interface.ts b/src/contact-properties/interfaces/update-contact-property-options.interface.ts new file mode 100644 index 00000000..7befa0aa --- /dev/null +++ b/src/contact-properties/interfaces/update-contact-property-options.interface.ts @@ -0,0 +1,23 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { ContactProperty } from './contact-property'; + +// SDK options +export type UpdateContactPropertyOptions = { + id: string; + fallbackValue?: string | number | null; +}; + +// API options +export interface UpdateContactPropertyApiOptions { + fallback_value?: string | number | null; +} + +export type UpdateContactPropertyResponseSuccess = Pick< + ContactProperty, + 'id' | 'object' +>; + +export interface UpdateContactPropertyResponse { + data: UpdateContactPropertyResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/resend.ts b/src/resend.ts index 23ba683d..230ac50b 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -6,6 +6,7 @@ import { Broadcasts } from './broadcasts/broadcasts'; import type { GetOptions, PostOptions, PutOptions } from './common/interfaces'; import type { IdempotentRequest } from './common/interfaces/idempotent-request.interface'; import type { PatchOptions } from './common/interfaces/patch-option.interface'; +import { ContactProperties } from './contact-properties/contact-properties'; import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; @@ -39,6 +40,7 @@ export class Resend { readonly batch = new Batch(this); readonly broadcasts = new Broadcasts(this); readonly contacts = new Contacts(this); + readonly contactProperties = new ContactProperties(this); readonly domains = new Domains(this); readonly emails = new Emails(this); readonly webhooks = new Webhooks(); From 7ce31db10fa703049a70fc5e64eabd867d97ea35 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Wed, 22 Oct 2025 13:21:26 -0300 Subject: [PATCH 39/49] feat: add sdk methods for webhooks (#698) --- src/index.ts | 1 + src/resend.ts | 2 +- .../create-webhook-options.interface.ts | 24 ++ .../interfaces/get-webhook.interface.ts | 20 ++ src/webhooks/interfaces/index.ts | 16 ++ .../interfaces/list-webhooks.interface.ts | 28 +++ src/webhooks/webhooks.spec.ts | 235 ++++++++++++++++-- src/webhooks/webhooks.ts | 47 ++++ 8 files changed, 353 insertions(+), 20 deletions(-) create mode 100644 src/webhooks/interfaces/create-webhook-options.interface.ts create mode 100644 src/webhooks/interfaces/get-webhook.interface.ts create mode 100644 src/webhooks/interfaces/index.ts create mode 100644 src/webhooks/interfaces/list-webhooks.interface.ts diff --git a/src/index.ts b/src/index.ts index 838e4a85..74abf5b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,4 @@ export * from './emails/receiving/interfaces'; export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; export * from './segments/interfaces'; +export * from './webhooks/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index 230ac50b..59d054a0 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -43,7 +43,7 @@ export class Resend { readonly contactProperties = new ContactProperties(this); readonly domains = new Domains(this); readonly emails = new Emails(this); - readonly webhooks = new Webhooks(); + readonly webhooks = new Webhooks(this); readonly templates = new Templates(this); readonly topics = new Topics(this); diff --git a/src/webhooks/interfaces/create-webhook-options.interface.ts b/src/webhooks/interfaces/create-webhook-options.interface.ts new file mode 100644 index 00000000..e2136e1a --- /dev/null +++ b/src/webhooks/interfaces/create-webhook-options.interface.ts @@ -0,0 +1,24 @@ +import type { PostOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; + +export interface CreateWebhookOptions { + endpoint: string; + events: string[]; +} + +export interface CreateWebhookRequestOptions extends PostOptions {} + +export interface CreateWebhookResponseSuccess { + object: 'webhook'; + id: string; +} + +export type CreateWebhookResponse = + | { + data: CreateWebhookResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/webhooks/interfaces/get-webhook.interface.ts b/src/webhooks/interfaces/get-webhook.interface.ts new file mode 100644 index 00000000..47c8528b --- /dev/null +++ b/src/webhooks/interfaces/get-webhook.interface.ts @@ -0,0 +1,20 @@ +import type { ErrorResponse } from '../../interfaces'; + +export interface GetWebhookResponseSuccess { + object: 'webhook'; + id: string; + created_at: string; + status: string; + endpoint: string; + events: string[] | null; +} + +export type GetWebhookResponse = + | { + data: GetWebhookResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/webhooks/interfaces/index.ts b/src/webhooks/interfaces/index.ts new file mode 100644 index 00000000..72486006 --- /dev/null +++ b/src/webhooks/interfaces/index.ts @@ -0,0 +1,16 @@ +export type { + CreateWebhookOptions, + CreateWebhookRequestOptions, + CreateWebhookResponse, + CreateWebhookResponseSuccess, +} from './create-webhook-options.interface'; +export type { + GetWebhookResponse, + GetWebhookResponseSuccess, +} from './get-webhook.interface'; +export type { + ListWebhooksOptions, + ListWebhooksResponse, + ListWebhooksResponseSuccess, + Webhook, +} from './list-webhooks.interface'; diff --git a/src/webhooks/interfaces/list-webhooks.interface.ts b/src/webhooks/interfaces/list-webhooks.interface.ts new file mode 100644 index 00000000..11b24048 --- /dev/null +++ b/src/webhooks/interfaces/list-webhooks.interface.ts @@ -0,0 +1,28 @@ +import type { PaginationOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; + +export type ListWebhooksOptions = PaginationOptions; + +export interface Webhook { + id: string; + endpoint: string; + created_at: string; + status: string; + events: string[] | null; +} + +export type ListWebhooksResponseSuccess = { + object: 'list'; + has_more: boolean; + data: Webhook[]; +}; + +export type ListWebhooksResponse = + | { + data: ListWebhooksResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index 62908e43..6c44fcd6 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -1,6 +1,15 @@ import { Webhook } from 'svix'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Webhooks } from './webhooks'; +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { + CreateWebhookOptions, + CreateWebhookResponseSuccess, +} from './interfaces/create-webhook-options.interface'; +import type { GetWebhookResponseSuccess } from './interfaces/get-webhook.interface'; +import type { ListWebhooksResponseSuccess } from './interfaces/list-webhooks.interface'; const mocks = vi.hoisted(() => { const verify = vi.fn(); @@ -18,34 +27,222 @@ vi.mock('svix', () => ({ Webhook: mocks.webhookConstructor, })); +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + describe('Webhooks', () => { beforeEach(() => { vi.clearAllMocks(); mocks.verify.mockReset(); + fetchMock.resetMocks(); + }); + + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a webhook', async () => { + const payload: CreateWebhookOptions = { + endpoint: 'https://example.com/webhook', + events: ['email.sent', 'email.delivered'], + }; + const response: CreateWebhookResponseSuccess = { + object: 'webhook', + id: '430eed87-632a-4ea6-90db-0aace67ec228', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 201, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + }); + + describe('get', () => { + describe('when webhook not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Webhook endpoint not found', + statusCode: 404, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.webhooks.get('1234'); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Webhook endpoint not found", + "name": "not_found", + "statusCode": 404, + }, +} +`); + }); + }); + + it('gets a webhook', async () => { + const response: GetWebhookResponseSuccess = { + object: 'webhook', + id: '430eed87-632a-4ea6-90db-0aace67ec228', + created_at: '2023-06-21T06:10:36.144Z', + status: 'enabled', + endpoint: 'https://example.com/webhook', + events: ['email.sent', 'email.delivered'], + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.webhooks.get('1234')).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2023-06-21T06:10:36.144Z", + "endpoint": "https://example.com/webhook", + "events": [ + "email.sent", + "email.delivered", + ], + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + "status": "enabled", + }, + "error": null, +} +`); + }); }); - it('verifies payload using svix headers', () => { - const options = { - payload: '{"type":"email.sent"}', - headers: { - id: 'msg_123', - timestamp: '1713984875', - signature: 'v1,some-signature', - }, - webhookSecret: 'whsec_123', + describe('list', () => { + const response: ListWebhooksResponseSuccess = { + has_more: false, + object: 'list', + data: [ + { + id: '430eed87-632a-4ea6-90db-0aace67ec228', + endpoint: 'https://example.com/webhook', + created_at: '2023-06-21T06:10:36.144Z', + status: 'enabled', + events: ['email.sent', 'email.delivered'], + }, + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + endpoint: 'https://example.com/webhook2', + created_at: '2023-06-20T06:10:36.144Z', + status: 'enabled', + events: ['email.bounced'], + }, + ], }; - const expectedResult = { id: 'msg_123', status: 'verified' }; - mocks.verify.mockReturnValue(expectedResult); + describe('when no pagination options are provided', () => { + it('lists webhooks', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.webhooks.list(); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/webhooks', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('when pagination options are provided', () => { + it('passes limit param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.webhooks.list({ limit: 10 }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/webhooks?limit=10', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + }); + + describe('verify', () => { + it('verifies payload using svix headers', () => { + const options = { + payload: '{"type":"email.sent"}', + headers: { + id: 'msg_123', + timestamp: '1713984875', + signature: 'v1,some-signature', + }, + webhookSecret: 'whsec_123', + }; + + const expectedResult = { id: 'msg_123', status: 'verified' }; + mocks.verify.mockReturnValue(expectedResult); - const result = new Webhooks().verify(options); + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = resend.webhooks.verify(options); - expect(Webhook).toHaveBeenCalledWith(options.webhookSecret); - expect(mocks.verify).toHaveBeenCalledWith(options.payload, { - 'svix-id': options.headers.id, - 'svix-timestamp': options.headers.timestamp, - 'svix-signature': options.headers.signature, + expect(Webhook).toHaveBeenCalledWith(options.webhookSecret); + expect(mocks.verify).toHaveBeenCalledWith(options.payload, { + 'svix-id': options.headers.id, + 'svix-timestamp': options.headers.timestamp, + 'svix-signature': options.headers.signature, + }); + expect(result).toBe(expectedResult); }); - expect(result).toBe(expectedResult); }); }); diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 41f9f3fa..af97dd88 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -1,4 +1,21 @@ import { Webhook } from 'svix'; +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + CreateWebhookOptions, + CreateWebhookRequestOptions, + CreateWebhookResponse, + CreateWebhookResponseSuccess, +} from './interfaces/create-webhook-options.interface'; +import type { + GetWebhookResponse, + GetWebhookResponseSuccess, +} from './interfaces/get-webhook.interface'; +import type { + ListWebhooksOptions, + ListWebhooksResponse, + ListWebhooksResponseSuccess, +} from './interfaces/list-webhooks.interface'; interface Headers { id: string; @@ -13,6 +30,36 @@ interface VerifyWebhookOptions { } export class Webhooks { + constructor(private readonly resend: Resend) {} + + async create( + payload: CreateWebhookOptions, + options: CreateWebhookRequestOptions = {}, + ): Promise { + const data = await this.resend.post( + '/webhooks', + payload, + options, + ); + return data; + } + + async get(id: string): Promise { + const data = await this.resend.get( + `/webhooks/${id}`, + ); + + return data; + } + + async list(options: ListWebhooksOptions = {}): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/webhooks?${queryString}` : '/webhooks'; + + const data = await this.resend.get(url); + return data; + } + verify(payload: VerifyWebhookOptions) { const webhook = new Webhook(payload.webhookSecret); return webhook.verify(payload.payload, { From 075d14ea7711a0002c128575e051ae21f81531ff Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Wed, 22 Oct 2025 16:19:44 -0300 Subject: [PATCH 40/49] feat: add webhook removal method to SDK (#708) --- src/webhooks/interfaces/index.ts | 4 ++ .../interfaces/remove-webhook.interface.ts | 17 +++++ src/webhooks/webhooks.spec.ts | 66 +++++++++++++++++++ src/webhooks/webhooks.ts | 11 ++++ 4 files changed, 98 insertions(+) create mode 100644 src/webhooks/interfaces/remove-webhook.interface.ts diff --git a/src/webhooks/interfaces/index.ts b/src/webhooks/interfaces/index.ts index 72486006..0a8eeb49 100644 --- a/src/webhooks/interfaces/index.ts +++ b/src/webhooks/interfaces/index.ts @@ -14,3 +14,7 @@ export type { ListWebhooksResponseSuccess, Webhook, } from './list-webhooks.interface'; +export type { + RemoveWebhookResponse, + RemoveWebhookResponseSuccess, +} from './remove-webhook.interface'; diff --git a/src/webhooks/interfaces/remove-webhook.interface.ts b/src/webhooks/interfaces/remove-webhook.interface.ts new file mode 100644 index 00000000..cb342527 --- /dev/null +++ b/src/webhooks/interfaces/remove-webhook.interface.ts @@ -0,0 +1,17 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Webhook } from './list-webhooks.interface'; + +export type RemoveWebhookResponseSuccess = Pick & { + object: 'webhook'; + deleted: boolean; +}; + +export type RemoveWebhookResponse = + | { + data: RemoveWebhookResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index 6c44fcd6..0be47bb1 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -10,6 +10,7 @@ import type { } from './interfaces/create-webhook-options.interface'; import type { GetWebhookResponseSuccess } from './interfaces/get-webhook.interface'; import type { ListWebhooksResponseSuccess } from './interfaces/list-webhooks.interface'; +import type { RemoveWebhookResponseSuccess } from './interfaces/remove-webhook.interface'; const mocks = vi.hoisted(() => { const verify = vi.fn(); @@ -218,6 +219,71 @@ describe('Webhooks', () => { }); }); + describe('remove', () => { + it('removes a webhook', async () => { + const id = '430eed87-632a-4ea6-90db-0aace67ec228'; + const response: RemoveWebhookResponseSuccess = { + object: 'webhook', + id, + deleted: true, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.webhooks.remove(id)).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + describe('when webhook not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Webhook not found', + statusCode: 404, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.webhooks.remove('1234'); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Webhook not found", + "name": "not_found", + "statusCode": 404, + }, +} +`); + }); + }); + }); + describe('verify', () => { it('verifies payload using svix headers', () => { const options = { diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index af97dd88..20efacee 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -16,6 +16,10 @@ import type { ListWebhooksResponse, ListWebhooksResponseSuccess, } from './interfaces/list-webhooks.interface'; +import type { + RemoveWebhookResponse, + RemoveWebhookResponseSuccess, +} from './interfaces/remove-webhook.interface'; interface Headers { id: string; @@ -60,6 +64,13 @@ export class Webhooks { return data; } + async remove(id: string): Promise { + const data = await this.resend.delete( + `/webhooks/${id}`, + ); + return data; + } + verify(payload: VerifyWebhookOptions) { const webhook = new Webhook(payload.webhookSecret); return webhook.verify(payload.payload, { From 9d12239daec4451d82ab5fe2addd89381e536e75 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Wed, 22 Oct 2025 17:13:19 -0300 Subject: [PATCH 41/49] chore: bump to 6.3.0-canary.2 (#709) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbcce849..295dc295 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.3.0-canary.1", + "version": "6.3.0-canary.2", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From a83315e96d2fc22059df82d2a5d9ee93f09e0270 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Thu, 23 Oct 2025 07:47:20 -0300 Subject: [PATCH 42/49] feat: add sending attachments endpoints to SDK (#697) --- src/attachments/attachments.ts | 3 + .../{receiving => }/interfaces/attachment.ts | 2 +- .../interfaces/get-attachment.interface.ts | 9 +- .../{receiving => }/interfaces/index.ts | 3 +- .../interfaces/list-attachments.interface.ts | 10 +- src/attachments/receiving/receiving.spec.ts | 110 ++-- src/attachments/receiving/receiving.ts | 4 +- src/attachments/sending/sending.spec.ts | 475 ++++++++++++++++++ src/attachments/sending/sending.ts | 39 ++ src/index.ts | 2 +- 10 files changed, 588 insertions(+), 69 deletions(-) rename src/attachments/{receiving => }/interfaces/attachment.ts (83%) rename src/attachments/{receiving => }/interfaces/get-attachment.interface.ts (58%) rename src/attachments/{receiving => }/interfaces/index.ts (78%) rename src/attachments/{receiving => }/interfaces/list-attachments.interface.ts (64%) create mode 100644 src/attachments/sending/sending.spec.ts create mode 100644 src/attachments/sending/sending.ts diff --git a/src/attachments/attachments.ts b/src/attachments/attachments.ts index 80e48f52..76819ce4 100644 --- a/src/attachments/attachments.ts +++ b/src/attachments/attachments.ts @@ -1,10 +1,13 @@ import type { Resend } from '../resend'; import { Receiving } from './receiving/receiving'; +import { Sending } from './sending/sending'; export class Attachments { readonly receiving: Receiving; + readonly sending: Sending; constructor(resend: Resend) { this.receiving = new Receiving(resend); + this.sending = new Sending(resend); } } diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/interfaces/attachment.ts similarity index 83% rename from src/attachments/receiving/interfaces/attachment.ts rename to src/attachments/interfaces/attachment.ts index 3414aca7..46bd8047 100644 --- a/src/attachments/receiving/interfaces/attachment.ts +++ b/src/attachments/interfaces/attachment.ts @@ -1,4 +1,4 @@ -export interface InboundAttachment { +export interface Attachment { id: string; filename?: string; size: number; diff --git a/src/attachments/receiving/interfaces/get-attachment.interface.ts b/src/attachments/interfaces/get-attachment.interface.ts similarity index 58% rename from src/attachments/receiving/interfaces/get-attachment.interface.ts rename to src/attachments/interfaces/get-attachment.interface.ts index 4c10e879..a513ea06 100644 --- a/src/attachments/receiving/interfaces/get-attachment.interface.ts +++ b/src/attachments/interfaces/get-attachment.interface.ts @@ -1,15 +1,14 @@ -import type { ErrorResponse } from '../../../interfaces'; -import type { InboundAttachment } from './attachment'; +import type { ErrorResponse } from '../../interfaces'; +import type { Attachment } from './attachment'; export interface GetAttachmentOptions { emailId: string; id: string; } -export interface GetAttachmentResponseSuccess { +export type GetAttachmentResponseSuccess = { object: 'attachment'; - data: InboundAttachment; -} +} & Attachment; export type GetAttachmentResponse = | { diff --git a/src/attachments/receiving/interfaces/index.ts b/src/attachments/interfaces/index.ts similarity index 78% rename from src/attachments/receiving/interfaces/index.ts rename to src/attachments/interfaces/index.ts index ec3f8400..6ee973d4 100644 --- a/src/attachments/receiving/interfaces/index.ts +++ b/src/attachments/interfaces/index.ts @@ -1,10 +1,11 @@ -export type { InboundAttachment } from './attachment'; export type { GetAttachmentOptions, GetAttachmentResponse, GetAttachmentResponseSuccess, } from './get-attachment.interface'; export type { + ListAttachmentsApiResponse, ListAttachmentsOptions, ListAttachmentsResponse, + ListAttachmentsResponseSuccess, } from './list-attachments.interface'; diff --git a/src/attachments/receiving/interfaces/list-attachments.interface.ts b/src/attachments/interfaces/list-attachments.interface.ts similarity index 64% rename from src/attachments/receiving/interfaces/list-attachments.interface.ts rename to src/attachments/interfaces/list-attachments.interface.ts index 94c36dca..35401658 100644 --- a/src/attachments/receiving/interfaces/list-attachments.interface.ts +++ b/src/attachments/interfaces/list-attachments.interface.ts @@ -1,6 +1,6 @@ -import type { PaginationOptions } from '../../../common/interfaces'; -import type { ErrorResponse } from '../../../interfaces'; -import type { InboundAttachment } from './attachment'; +import type { PaginationOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; +import type { Attachment } from './attachment'; export type ListAttachmentsOptions = PaginationOptions & { emailId: string; @@ -9,13 +9,13 @@ export type ListAttachmentsOptions = PaginationOptions & { export interface ListAttachmentsApiResponse { object: 'list'; has_more: boolean; - data: InboundAttachment[]; + data: Attachment[]; } export interface ListAttachmentsResponseSuccess { object: 'list'; has_more: boolean; - data: InboundAttachment[]; + data: Attachment[]; } export type ListAttachmentsResponse = diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts index 74253f2b..75bdd429 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -5,7 +5,7 @@ import { mockSuccessResponse } from '../../test-utils/mock-fetch'; import type { ListAttachmentsApiResponse, ListAttachmentsResponseSuccess, -} from './interfaces/list-attachments.interface'; +} from '../interfaces'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); @@ -53,15 +53,14 @@ describe('Receiving', () => { it('returns attachment with download URL', async () => { const apiResponse = { object: 'attachment' as const, - data: { - id: 'att_123', - filename: 'document.pdf', - content_type: 'application/pdf', - content_id: 'cid_123', - content_disposition: 'attachment' as const, - download_url: 'https://example.com/download/att_123', - expires_at: '2025-10-18T12:00:00Z', - }, + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }; fetchMock.mockOnceIf( @@ -83,16 +82,15 @@ describe('Receiving', () => { expect(result).toEqual({ data: { - data: { - content_disposition: 'attachment', - content_id: 'cid_123', - content_type: 'application/pdf', - download_url: 'https://example.com/download/att_123', - expires_at: '2025-10-18T12:00:00Z', - filename: 'document.pdf', - id: 'att_123', - }, object: 'attachment', + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_disposition: 'attachment', + content_id: 'cid_123', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', }, error: null, }); @@ -101,15 +99,14 @@ describe('Receiving', () => { it('returns inline attachment with download URL', async () => { const apiResponse = { object: 'attachment' as const, - data: { - id: 'att_456', - filename: 'image.png', - content_type: 'image/png', - content_id: 'cid_456', - content_disposition: 'inline' as const, - download_url: 'https://example.com/download/att_456', - expires_at: '2025-10-18T12:00:00Z', - }, + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }; fetchMock.mockOnceIf( @@ -131,16 +128,15 @@ describe('Receiving', () => { expect(result).toEqual({ data: { - data: { - content_disposition: 'inline', - content_id: 'cid_456', - content_type: 'image/png', - download_url: 'https://example.com/download/att_456', - expires_at: '2025-10-18T12:00:00Z', - filename: 'image.png', - id: 'att_456', - }, object: 'attachment', + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_disposition: 'inline', + content_id: 'cid_456', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', }, error: null, }); @@ -149,15 +145,12 @@ describe('Receiving', () => { it('handles attachment without optional fields (filename, contentId)', async () => { const apiResponse = { object: 'attachment' as const, - data: { - // Required fields based on DB schema - id: 'att_789', - content_type: 'text/plain', - content_disposition: 'attachment' as const, - download_url: 'https://example.com/download/att_789', - expires_at: '2025-10-18T12:00:00Z', - // Optional fields (filename, content_id) omitted - }, + id: 'att_789', + size: 512, + content_type: 'text/plain', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', }; fetchMock.mockOnceIf( @@ -179,14 +172,13 @@ describe('Receiving', () => { expect(result).toEqual({ data: { - data: { - content_disposition: 'attachment', - content_type: 'text/plain', - download_url: 'https://example.com/download/att_789', - expires_at: '2025-10-18T12:00:00Z', - id: 'att_789', - }, object: 'attachment', + id: 'att_789', + size: 512, + content_type: 'text/plain', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', }, error: null, }); @@ -202,6 +194,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment' as const, @@ -211,6 +204,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline' as const, @@ -272,6 +266,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', @@ -281,6 +276,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', @@ -337,6 +333,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', @@ -346,6 +343,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', @@ -381,6 +379,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', @@ -390,6 +389,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', @@ -423,6 +423,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', @@ -432,6 +433,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', @@ -465,6 +467,7 @@ describe('Receiving', () => { { id: 'att_123', filename: 'document.pdf', + size: 2048, content_type: 'application/pdf', content_id: 'cid_123', content_disposition: 'attachment', @@ -474,6 +477,7 @@ describe('Receiving', () => { { id: 'att_456', filename: 'image.png', + size: 1536, content_type: 'image/png', content_id: 'cid_456', content_disposition: 'inline', diff --git a/src/attachments/receiving/receiving.ts b/src/attachments/receiving/receiving.ts index 8a7ab310..64945079 100644 --- a/src/attachments/receiving/receiving.ts +++ b/src/attachments/receiving/receiving.ts @@ -4,12 +4,10 @@ import type { GetAttachmentOptions, GetAttachmentResponse, GetAttachmentResponseSuccess, -} from './interfaces/get-attachment.interface'; -import type { ListAttachmentsApiResponse, ListAttachmentsOptions, ListAttachmentsResponse, -} from './interfaces/list-attachments.interface'; +} from '../interfaces'; export class Receiving { constructor(private readonly resend: Resend) {} diff --git a/src/attachments/sending/sending.spec.ts b/src/attachments/sending/sending.spec.ts new file mode 100644 index 00000000..f345db3f --- /dev/null +++ b/src/attachments/sending/sending.spec.ts @@ -0,0 +1,475 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../../interfaces'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + ListAttachmentsApiResponse, + ListAttachmentsResponseSuccess, +} from '../interfaces'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Sending', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('get', () => { + describe('when attachment not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Attachment not found', + statusCode: 404, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.sending.get({ + emailId: '61cda979-919d-4b9d-9638-c148b93ff410', + id: 'att_123', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Attachment not found", + "name": "not_found", + "statusCode": 404, + }, +} +`); + }); + }); + + describe('when attachment found', () => { + it('returns attachment with download URL', async () => { + const apiResponse = { + object: 'attachment' as const, + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }; + + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_123', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const result = await resend.attachments.sending.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_123', + }); + + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + }); + + it('returns inline attachment with download URL', async () => { + const apiResponse = { + object: 'attachment' as const, + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }; + + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_456', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const result = await resend.attachments.sending.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_456', + }); + + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + }); + + it('handles attachment without optional fields (filename, contentId)', async () => { + const apiResponse = { + object: 'attachment' as const, + id: 'att_789', + size: 512, + content_type: 'text/plain', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', + }; + + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments/att_789', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const result = await resend.attachments.sending.get({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + id: 'att_789', + }); + + expect(result).toEqual({ + data: apiResponse, + error: null, + }); + }); + }); + }); + + describe('list', () => { + const apiResponse: ListAttachmentsApiResponse = { + object: 'list' as const, + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + const headers = { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }; + + describe('when email not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Email not found', + statusCode: 404, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.attachments.sending.list({ + emailId: '61cda979-919d-4b9d-9638-c148b93ff410', + }); + + expect(result).toEqual({ data: null, error: response }); + }); + }); + + describe('when attachments found', () => { + it('returns multiple attachments with download URLs', async () => { + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + JSON.stringify(apiResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + expect(result).toEqual({ data: expectedResponse, error: null }); + }); + + it('returns empty array when no attachments', async () => { + const emptyResponse = { + object: 'list' as const, + has_more: false, + data: [], + }; + + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + JSON.stringify(emptyResponse), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }, + ); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + expect(result).toEqual({ data: emptyResponse, error: null }); + }); + }); + + describe('when no pagination options provided', () => { + it('calls endpoint without query params and return the response', async () => { + mockSuccessResponse(apiResponse, { + headers, + }); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + expect(result).toEqual({ + data: expectedResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments', + ); + }); + }); + + describe('when pagination options are provided', () => { + it('calls endpoint passing limit param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + limit: 10, + }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + expect(result).toEqual({ + data: expectedResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?limit=10', + ); + }); + + it('calls endpoint passing after param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + after: 'cursor123', + }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + expect(result).toEqual({ + data: expectedResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?after=cursor123', + ); + }); + + it('calls endpoint passing before param and return the response', async () => { + mockSuccessResponse(apiResponse, { headers }); + + const result = await resend.attachments.sending.list({ + emailId: '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + before: 'cursor123', + }); + + const expectedResponse: ListAttachmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'att_123', + filename: 'document.pdf', + size: 2048, + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + size: 1536, + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline', + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + expect(result).toEqual({ + data: expectedResponse, + error: null, + }); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/emails/sending/67d9bcdb-5a02-42d7-8da9-0d6feea18cff/attachments?before=cursor123', + ); + }); + }); + }); +}); diff --git a/src/attachments/sending/sending.ts b/src/attachments/sending/sending.ts new file mode 100644 index 00000000..7f2b58da --- /dev/null +++ b/src/attachments/sending/sending.ts @@ -0,0 +1,39 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + GetAttachmentOptions, + GetAttachmentResponse, + GetAttachmentResponseSuccess, + ListAttachmentsApiResponse, + ListAttachmentsOptions, + ListAttachmentsResponse, +} from '../interfaces'; + +export class Sending { + constructor(private readonly resend: Resend) {} + + async get(options: GetAttachmentOptions): Promise { + const { emailId, id } = options; + + const data = await this.resend.get( + `/emails/sending/${emailId}/attachments/${id}`, + ); + + return data; + } + + async list( + options: ListAttachmentsOptions, + ): Promise { + const { emailId } = options; + + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/emails/sending/${emailId}/attachments?${queryString}` + : `/emails/sending/${emailId}/attachments`; + + const data = await this.resend.get(url); + + return data; + } +} diff --git a/src/index.ts b/src/index.ts index 74abf5b8..8fcbf44e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './api-keys/interfaces'; -export * from './attachments/receiving/interfaces'; +export * from './attachments/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; export * from './common/interfaces'; From 36e3f4507d342f17ebe1efc8bab42f56a885d1a1 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 23 Oct 2025 11:24:51 -0300 Subject: [PATCH 43/49] feat: add webhook signing_secret to webhook response types (#710) --- package.json | 2 +- src/webhooks/interfaces/create-webhook-options.interface.ts | 1 + src/webhooks/interfaces/get-webhook.interface.ts | 1 + src/webhooks/webhooks.spec.ts | 4 ++++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 295dc295..073ec0c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.3.0-canary.2", + "version": "6.3.0-canary.3", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/src/webhooks/interfaces/create-webhook-options.interface.ts b/src/webhooks/interfaces/create-webhook-options.interface.ts index e2136e1a..15296cbd 100644 --- a/src/webhooks/interfaces/create-webhook-options.interface.ts +++ b/src/webhooks/interfaces/create-webhook-options.interface.ts @@ -11,6 +11,7 @@ export interface CreateWebhookRequestOptions extends PostOptions {} export interface CreateWebhookResponseSuccess { object: 'webhook'; id: string; + signing_secret: string; } export type CreateWebhookResponse = diff --git a/src/webhooks/interfaces/get-webhook.interface.ts b/src/webhooks/interfaces/get-webhook.interface.ts index 47c8528b..9cedef19 100644 --- a/src/webhooks/interfaces/get-webhook.interface.ts +++ b/src/webhooks/interfaces/get-webhook.interface.ts @@ -7,6 +7,7 @@ export interface GetWebhookResponseSuccess { status: string; endpoint: string; events: string[] | null; + signing_secret: string; } export type GetWebhookResponse = diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index 0be47bb1..d0c0afe9 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -49,6 +49,7 @@ describe('Webhooks', () => { const response: CreateWebhookResponseSuccess = { object: 'webhook', id: '430eed87-632a-4ea6-90db-0aace67ec228', + signing_secret: 'whsec_test_secret_key_123', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -68,6 +69,7 @@ describe('Webhooks', () => { "data": { "id": "430eed87-632a-4ea6-90db-0aace67ec228", "object": "webhook", + "signing_secret": "whsec_test_secret_key_123", }, "error": null, } @@ -117,6 +119,7 @@ describe('Webhooks', () => { status: 'enabled', endpoint: 'https://example.com/webhook', events: ['email.sent', 'email.delivered'], + signing_secret: 'whsec_test_secret_key_123', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -140,6 +143,7 @@ describe('Webhooks', () => { ], "id": "430eed87-632a-4ea6-90db-0aace67ec228", "object": "webhook", + "signing_secret": "whsec_test_secret_key_123", "status": "enabled", }, "error": null, From a673763ecd8cc47837609290a4dcccb61ec4167e Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 23 Oct 2025 18:42:15 -0300 Subject: [PATCH 44/49] feat: add webhook update endpoint (#712) --- package.json | 2 +- .../create-webhook-options.interface.ts | 3 +- .../interfaces/get-webhook.interface.ts | 5 +- src/webhooks/interfaces/index.ts | 6 + .../interfaces/list-webhooks.interface.ts | 5 +- .../interfaces/update-webhook.interface.ts | 23 ++ .../interfaces/webhook-event.interface.ts | 16 ++ src/webhooks/webhooks.spec.ts | 235 ++++++++++++++++++ src/webhooks/webhooks.ts | 16 ++ 9 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 src/webhooks/interfaces/update-webhook.interface.ts create mode 100644 src/webhooks/interfaces/webhook-event.interface.ts diff --git a/package.json b/package.json index 073ec0c3..82f42cb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.3.0-canary.3", + "version": "6.3.0-canary.4", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/src/webhooks/interfaces/create-webhook-options.interface.ts b/src/webhooks/interfaces/create-webhook-options.interface.ts index 15296cbd..4eccebef 100644 --- a/src/webhooks/interfaces/create-webhook-options.interface.ts +++ b/src/webhooks/interfaces/create-webhook-options.interface.ts @@ -1,9 +1,10 @@ import type { PostOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; +import type { WebhookEvent } from './webhook-event.interface'; export interface CreateWebhookOptions { endpoint: string; - events: string[]; + events: WebhookEvent[]; } export interface CreateWebhookRequestOptions extends PostOptions {} diff --git a/src/webhooks/interfaces/get-webhook.interface.ts b/src/webhooks/interfaces/get-webhook.interface.ts index 9cedef19..87d096dd 100644 --- a/src/webhooks/interfaces/get-webhook.interface.ts +++ b/src/webhooks/interfaces/get-webhook.interface.ts @@ -1,12 +1,13 @@ import type { ErrorResponse } from '../../interfaces'; +import type { WebhookEvent } from './webhook-event.interface'; export interface GetWebhookResponseSuccess { object: 'webhook'; id: string; created_at: string; - status: string; + status: 'enabled' | 'disabled'; endpoint: string; - events: string[] | null; + events: WebhookEvent[] | null; signing_secret: string; } diff --git a/src/webhooks/interfaces/index.ts b/src/webhooks/interfaces/index.ts index 0a8eeb49..2a489323 100644 --- a/src/webhooks/interfaces/index.ts +++ b/src/webhooks/interfaces/index.ts @@ -18,3 +18,9 @@ export type { RemoveWebhookResponse, RemoveWebhookResponseSuccess, } from './remove-webhook.interface'; +export type { + UpdateWebhookOptions, + UpdateWebhookResponse, + UpdateWebhookResponseSuccess, +} from './update-webhook.interface'; +export type { WebhookEvent } from './webhook-event.interface'; diff --git a/src/webhooks/interfaces/list-webhooks.interface.ts b/src/webhooks/interfaces/list-webhooks.interface.ts index 11b24048..462edd41 100644 --- a/src/webhooks/interfaces/list-webhooks.interface.ts +++ b/src/webhooks/interfaces/list-webhooks.interface.ts @@ -1,5 +1,6 @@ import type { PaginationOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; +import type { WebhookEvent } from './webhook-event.interface'; export type ListWebhooksOptions = PaginationOptions; @@ -7,8 +8,8 @@ export interface Webhook { id: string; endpoint: string; created_at: string; - status: string; - events: string[] | null; + status: 'enabled' | 'disabled'; + events: WebhookEvent[] | null; } export type ListWebhooksResponseSuccess = { diff --git a/src/webhooks/interfaces/update-webhook.interface.ts b/src/webhooks/interfaces/update-webhook.interface.ts new file mode 100644 index 00000000..29d144e9 --- /dev/null +++ b/src/webhooks/interfaces/update-webhook.interface.ts @@ -0,0 +1,23 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { WebhookEvent } from './webhook-event.interface'; + +export interface UpdateWebhookOptions { + endpoint?: string; + events?: WebhookEvent[]; + status?: 'enabled' | 'disabled'; +} + +export interface UpdateWebhookResponseSuccess { + object: 'webhook'; + id: string; +} + +export type UpdateWebhookResponse = + | { + data: UpdateWebhookResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/webhooks/interfaces/webhook-event.interface.ts b/src/webhooks/interfaces/webhook-event.interface.ts new file mode 100644 index 00000000..2c052e7a --- /dev/null +++ b/src/webhooks/interfaces/webhook-event.interface.ts @@ -0,0 +1,16 @@ +export type WebhookEvent = + | 'email.sent' + | 'email.delivered' + | 'email.delivery_delayed' + | 'email.complained' + | 'email.bounced' + | 'email.opened' + | 'email.clicked' + | 'email.received' + | 'email.failed' + | 'contact.created' + | 'contact.updated' + | 'contact.deleted' + | 'domain.created' + | 'domain.updated' + | 'domain.deleted'; diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index d0c0afe9..7f601450 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -11,6 +11,10 @@ import type { import type { GetWebhookResponseSuccess } from './interfaces/get-webhook.interface'; import type { ListWebhooksResponseSuccess } from './interfaces/list-webhooks.interface'; import type { RemoveWebhookResponseSuccess } from './interfaces/remove-webhook.interface'; +import type { + UpdateWebhookOptions, + UpdateWebhookResponseSuccess, +} from './interfaces/update-webhook.interface'; const mocks = vi.hoisted(() => { const verify = vi.fn(); @@ -223,6 +227,237 @@ describe('Webhooks', () => { }); }); + describe('update', () => { + const webhookId = '430eed87-632a-4ea6-90db-0aace67ec228'; + + it('updates all webhook fields', async () => { + const payload: UpdateWebhookOptions = { + endpoint: 'https://new.com/webhook', + events: ['email.sent', 'email.delivered', 'email.bounced'], + status: 'disabled', + }; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + it('updates only endpoint field', async () => { + const payload: UpdateWebhookOptions = { + endpoint: 'https://new.com/webhook', + }; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + it('updates only events field', async () => { + const payload: UpdateWebhookOptions = { + events: ['email.sent', 'email.delivered'], + }; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + it('updates only status field to disabled', async () => { + const payload: UpdateWebhookOptions = { + status: 'disabled', + }; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + it('updates only status field to enabled', async () => { + const payload: UpdateWebhookOptions = { + status: 'enabled', + }; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + it('handles empty payload', async () => { + const payload: UpdateWebhookOptions = {}; + const response: UpdateWebhookResponseSuccess = { + object: 'webhook', + id: webhookId, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.webhooks.update(webhookId, payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "430eed87-632a-4ea6-90db-0aace67ec228", + "object": "webhook", + }, + "error": null, +} +`); + }); + + describe('when webhook not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Failed to update webhook endpoint', + statusCode: 404, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.webhooks.update(webhookId, { + endpoint: 'https://new.com/webhook', + }); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Failed to update webhook endpoint", + "name": "not_found", + "statusCode": 404, + }, +} +`); + }); + }); + }); + describe('remove', () => { it('removes a webhook', async () => { const id = '430eed87-632a-4ea6-90db-0aace67ec228'; diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 20efacee..841579ea 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -20,6 +20,11 @@ import type { RemoveWebhookResponse, RemoveWebhookResponseSuccess, } from './interfaces/remove-webhook.interface'; +import type { + UpdateWebhookOptions, + UpdateWebhookResponse, + UpdateWebhookResponseSuccess, +} from './interfaces/update-webhook.interface'; interface Headers { id: string; @@ -64,6 +69,17 @@ export class Webhooks { return data; } + async update( + id: string, + payload: UpdateWebhookOptions, + ): Promise { + const data = await this.resend.patch( + `/webhooks/${id}`, + payload, + ); + return data; + } + async remove(id: string): Promise { const data = await this.resend.delete( `/webhooks/${id}`, From 075fa0c70106e68235e29cb5f176a2f5055624ce Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Sat, 25 Oct 2025 07:31:55 -0300 Subject: [PATCH 45/49] feat: autocomplete for testing email addresses --- .../create-email-options.interface.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index 5aa9afe0..ffd27d49 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -32,9 +32,15 @@ interface EmailTemplateOptions { }; } +type FromTestingAddress = 'onboarding@resend.dev'; +type ToTestingAddress = + | 'delivered@resend.dev' + | 'bounced@resend.dev' + | 'complained@resend.dev'; + interface CreateEmailBaseOptionsWithTemplate extends Omit { - from?: string; + from?: FromTestingAddress | (string & {}); subject?: string; } @@ -62,7 +68,7 @@ interface CreateEmailBaseOptions { * * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters */ - from: string; + from: FromTestingAddress | (string & {}); /** * Custom headers to add to the email. * @@ -92,7 +98,7 @@ interface CreateEmailBaseOptions { * * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters */ - to: string | string[]; + to: ToTestingAddress | (string & {}) | (ToTestingAddress | (string & {}))[]; /** * The id of the topic you want to send to * @@ -110,17 +116,17 @@ interface CreateEmailBaseOptions { export type CreateEmailOptions = | ((RequireAtLeastOne & CreateEmailBaseOptions) & { - template?: never; - }) + template?: never; + }) | ((EmailTemplateOptions & CreateEmailBaseOptionsWithTemplate) & { - react?: never; - html?: never; - text?: never; - }); + react?: never; + html?: never; + text?: never; + }); export interface CreateEmailRequestOptions extends PostOptions, - IdempotentRequest {} + IdempotentRequest { } export interface CreateEmailResponseSuccess { /** The ID of the newly created email. */ @@ -129,13 +135,13 @@ export interface CreateEmailResponseSuccess { export type CreateEmailResponse = | { - data: CreateEmailResponseSuccess; - error: null; - } + data: CreateEmailResponseSuccess; + error: null; + } | { - data: null; - error: ErrorResponse; - }; + data: null; + error: ErrorResponse; + }; export interface Attachment { /** Content of an attached file. */ From 55ce0540371efc453380ceff28610510b20f2fb4 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Sat, 25 Oct 2025 07:33:30 -0300 Subject: [PATCH 46/49] lint --- .../create-email-options.interface.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index ffd27d49..c9cf8aa1 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -116,17 +116,17 @@ interface CreateEmailBaseOptions { export type CreateEmailOptions = | ((RequireAtLeastOne & CreateEmailBaseOptions) & { - template?: never; - }) + template?: never; + }) | ((EmailTemplateOptions & CreateEmailBaseOptionsWithTemplate) & { - react?: never; - html?: never; - text?: never; - }); + react?: never; + html?: never; + text?: never; + }); export interface CreateEmailRequestOptions extends PostOptions, - IdempotentRequest { } + IdempotentRequest {} export interface CreateEmailResponseSuccess { /** The ID of the newly created email. */ @@ -135,13 +135,13 @@ export interface CreateEmailResponseSuccess { export type CreateEmailResponse = | { - data: CreateEmailResponseSuccess; - error: null; - } + data: CreateEmailResponseSuccess; + error: null; + } | { - data: null; - error: ErrorResponse; - }; + data: null; + error: ErrorResponse; + }; export interface Attachment { /** Content of an attached file. */ From 8e2c45be66dd714735b728f7892ca89e67af6e02 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 27 Oct 2025 11:49:47 -0300 Subject: [PATCH 47/49] keepduplicated export, --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 8fcbf44e..cf297606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,3 +11,4 @@ export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; export * from './segments/interfaces'; export * from './webhooks/interfaces'; +export * from './webhooks/interfaces'; From 9995f732b2eb2a48d4fc3090abbb70fc24bc077b Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 29 Oct 2025 09:07:21 -0300 Subject: [PATCH 48/49] add js doc mentioning the testing email addressers --- .../create-email-options.interface.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index c9cf8aa1..846644ff 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -32,7 +32,17 @@ interface EmailTemplateOptions { }; } +/** + * These are testing email addresses, they do not actually send an email to anyone. + * + * @link https://resend.com/docs/dashboard/emails/send-test-emails + */ type FromTestingAddress = 'onboarding@resend.dev'; +/** + * These are testing email addresses, they do not actually send an email to anyone. + * + * @link https://resend.com/docs/dashboard/emails/send-test-emails + */ type ToTestingAddress = | 'delivered@resend.dev' | 'bounced@resend.dev' @@ -97,6 +107,13 @@ interface CreateEmailBaseOptions { * Recipient email address. For multiple addresses, send as an array of strings. Max 50. * * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters + * + * Can also be one of the following testing email addresses + * - `delivered@resend.dev` + * - `bounced@resend.dev` + * - `complained@resend.dev` + * + * @link https://resend.com/docs/dashboard/emails/send-test-emails */ to: ToTestingAddress | (string & {}) | (ToTestingAddress | (string & {}))[]; /** From ccfc6f3b74ecd686bcf6ff2435271c97803e90a5 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 13 Nov 2025 11:42:24 -0300 Subject: [PATCH 49/49] don't add autocomplete for the from field --- .../interfaces/create-email-options.interface.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index 846644ff..1d7e6b04 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -32,17 +32,6 @@ interface EmailTemplateOptions { }; } -/** - * These are testing email addresses, they do not actually send an email to anyone. - * - * @link https://resend.com/docs/dashboard/emails/send-test-emails - */ -type FromTestingAddress = 'onboarding@resend.dev'; -/** - * These are testing email addresses, they do not actually send an email to anyone. - * - * @link https://resend.com/docs/dashboard/emails/send-test-emails - */ type ToTestingAddress = | 'delivered@resend.dev' | 'bounced@resend.dev' @@ -50,7 +39,7 @@ type ToTestingAddress = interface CreateEmailBaseOptionsWithTemplate extends Omit { - from?: FromTestingAddress | (string & {}); + from?: string; subject?: string; } @@ -78,7 +67,7 @@ interface CreateEmailBaseOptions { * * @link https://resend.com/docs/api-reference/emails/send-email#body-parameters */ - from: FromTestingAddress | (string & {}); + from: string; /** * Custom headers to add to the email. *