From 4290ccb237e1fa82e508541991fc859f01fd4465 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Tue, 30 Sep 2025 11:52:02 -0300 Subject: [PATCH 01/25] 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 a466cfdf..c52a3bce 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'; const defaultBaseUrl = 'https://api.resend.com'; @@ -26,12 +28,14 @@ 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); readonly contacts = new Contacts(this); readonly domains = new Domains(this); readonly emails = new Emails(this); + readonly inbound = new Inbound(this); constructor(readonly key?: string) { if (!key) { From 3e9e39cd3e64e87aeb6a2f69e68afe8616721f87 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/25] 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 5aca9785..f19d1aad 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,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 a654a86d..48421e7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: version: 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.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 @@ -51,55 +51,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] @@ -1509,39 +1509,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 24d4745153686530d13291a7396264725245353c Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Tue, 30 Sep 2025 11:56:50 -0300 Subject: [PATCH 03/25] 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 f19d1aad..fce7a358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.1.2", + "version": "6.2.0-canary.0", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", From 79ac64d579234798ed6813b56133bac86a6fc616 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 1 Oct 2025 10:22:34 -0300 Subject: [PATCH 04/25] 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 fce7a358..10611425 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 12734ecb2411b980e75766fee0c174a7d62ba990 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/25] 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 10611425..70e7ea7c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,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 48421e7c..a89af165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,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 @@ -32,10 +32,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: @@ -728,8 +728,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==} @@ -1953,7 +1953,7 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@22.18.6': + '@types/node@22.18.8': dependencies: undici-types: 6.21.0 @@ -1969,13 +1969,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: @@ -2590,13 +2590,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 @@ -2611,7 +2611,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) @@ -2620,11 +2620,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) @@ -2633,19 +2633,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 @@ -2663,11 +2663,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 207a4aaf99f98c545f3edfcb99d98521304d99ae 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/25] 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 70e7ea7c..34dafbda 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,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 a89af165..30a7fbe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,10 +26,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) @@ -1303,8 +1303,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 @@ -2540,7 +2540,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 @@ -2561,7 +2561,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.6 - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color @@ -2572,7 +2572,7 @@ snapshots: type-detect@4.1.0: {} - typescript@5.9.2: {} + typescript@5.9.3: {} ufo@1.6.1: {} From 7920f0e65f0f0c9eb3c2f641dee82984b083d180 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/25] 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 291538213452cf2d6ea8623ad650341442a08f81 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/25] 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 34dafbda..94aadaf5 100644 --- a/package.json +++ b/package.json @@ -61,5 +61,5 @@ "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, - "packageManager": "pnpm@10.17.1" + "packageManager": "pnpm@10.18.0" } From 1e1ed85d570c409f0af97a75c490c659debb57c8 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/25] 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 94aadaf5..f9f20e6e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,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 30a7fbe0..5f96b487 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: version: 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.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 @@ -51,55 +51,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] @@ -1509,39 +1509,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 6b7529c535ee6338936840667c9b7ff659dc0001 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/25] 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 f9f20e6e..899d168d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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 5f96b487..ca5b0eee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,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 @@ -731,8 +731,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==} @@ -1957,7 +1957,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/react@19.1.15': + '@types/react@19.2.0': dependencies: csstype: 3.1.3 From 97dc4f1d39490f784d7e6dbf1e1e8e7a99424fff Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Sat, 4 Oct 2025 19:09:11 -0300 Subject: [PATCH 11/25] 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 1d74eae46c866345d469c6e3de3981e1649465da Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Sat, 4 Oct 2025 19:15:15 -0300 Subject: [PATCH 12/25] 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 899d168d..836814a0 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 b0491c42bd7fb885b0feb56afb6d5f9e39039cb2 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/25] 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 98c486173dca2474710956669970250ca38dc3fc Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 16:55:46 -0300 Subject: [PATCH 14/25] 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 c52a3bce..d83ee8f8 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'; const defaultBaseUrl = 'https://api.resend.com'; @@ -35,7 +34,6 @@ export class Resend { readonly contacts = new Contacts(this); readonly domains = new Domains(this); readonly emails = new Emails(this); - readonly inbound = new Inbound(this); constructor(readonly key?: string) { if (!key) { From f4fe9f274c9683d611fde5dbbbe0ea7837e05a05 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 16:57:27 -0300 Subject: [PATCH 15/25] 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 c2206014bb0bdb00ef9c52c934d4f8ed2f0f8733 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Thu, 9 Oct 2025 17:31:56 -0300 Subject: [PATCH 16/25] 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 9d592bcc2fd1e2f09ddc12b285e732472cb14350 Mon Sep 17 00:00:00 2001 From: Lucas da Costa Date: Fri, 10 Oct 2025 09:02:14 -0300 Subject: [PATCH 17/25] 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 4cb63b247ebb41357d4541c0b856adc658cd7961 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/25] 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 836814a0..cd012237 100644 --- a/package.json +++ b/package.json @@ -61,5 +61,5 @@ "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, - "packageManager": "pnpm@10.18.0" + "packageManager": "pnpm@10.18.2" } From 169874b3e95e5702e8b8d9b8d9f47807b746d19e 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/25] 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 cd012237..a29efb6d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,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 ca5b0eee..c4cacb37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,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 @@ -32,10 +32,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: @@ -728,8 +728,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==} @@ -1953,7 +1953,7 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@22.18.8': + '@types/node@22.18.9': dependencies: undici-types: 6.21.0 @@ -1969,13 +1969,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: @@ -2590,13 +2590,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 @@ -2611,7 +2611,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) @@ -2620,11 +2620,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) @@ -2633,19 +2633,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 @@ -2663,11 +2663,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 99bca1d613bcfe32ed9190c02501a572c2ed2a9f 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/25] 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 6f3b9ea58c45c1c1b56019129f8756dac986d274 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/25] feat: partial (API Keys, Audiences, and Contacts) non-mocked test coverage (#663) --- .gitignore | 1 + package.json | 10 +- pnpm-lock.yaml | 942 +++++++++++++++++ .../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, 9593 insertions(+), 6 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 a29efb6d..889cff1d 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", @@ -53,9 +55,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 c4cacb37..e530e68d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,15 +15,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) @@ -104,6 +122,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'} @@ -420,6 +441,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'} @@ -504,6 +533,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'} @@ -719,6 +769,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'} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -734,6 +788,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==} @@ -763,6 +820,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'} @@ -787,6 +848,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'} @@ -794,9 +858,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==} @@ -806,10 +884,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==} @@ -843,6 +933,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'} @@ -850,6 +964,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'} @@ -880,9 +1002,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==} @@ -896,22 +1026,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'} + esbuild@0.25.8: resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} @@ -922,16 +1083,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==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -945,6 +1120,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==} @@ -952,15 +1131,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'} @@ -968,10 +1190,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'} @@ -986,6 +1231,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'} @@ -993,6 +1242,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==} @@ -1007,21 +1259,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'} @@ -1036,6 +1331,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==} @@ -1047,10 +1349,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==} @@ -1060,6 +1386,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'} @@ -1068,6 +1398,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==} @@ -1123,10 +1460,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'} @@ -1135,10 +1484,21 @@ packages: resolution: {integrity: sha512-IQHOQ9aauHAApwAaUYifpEyLHv6fpVGVkMOnwPzcDScLjbLj8tLsILn6unSW79NafOw1llh8oK7Gd0VwmXBFmA==} engines: {node: '>=18'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quick-lru@7.1.0: 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: @@ -1155,10 +1515,18 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@5.0.0: 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'} @@ -1169,12 +1537,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'} @@ -1183,6 +1577,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==} @@ -1190,6 +1600,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'} @@ -1206,6 +1620,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==} @@ -1266,6 +1684,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==} @@ -1303,6 +1728,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'} @@ -1325,14 +1754,36 @@ 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} + 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'} + validate-npm-package-name@5.0.1: 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} @@ -1544,6 +1995,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 @@ -1702,6 +2155,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 @@ -1809,6 +2268,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 @@ -1945,6 +2461,8 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sindresorhus/fnv1a@2.0.1': {} + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -1961,6 +2479,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 @@ -2003,6 +2525,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: {} @@ -2017,12 +2544,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 @@ -2032,8 +2586,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: @@ -2062,6 +2628,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 @@ -2070,6 +2656,10 @@ snapshots: csstype@3.1.3: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2084,8 +2674,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 @@ -2104,16 +2698,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 + esbuild@0.25.8: optionalDependencies: '@esbuild/aix-ppc64': 0.25.8 @@ -2172,20 +2788,74 @@ 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: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 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 @@ -2197,9 +2867,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 @@ -2209,6 +2909,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 @@ -2224,8 +2943,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: {} @@ -2238,10 +2981,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: {} @@ -2250,12 +3003,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 @@ -2264,6 +3023,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 @@ -2284,6 +3063,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: @@ -2294,8 +3085,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 @@ -2307,6 +3114,8 @@ snapshots: leac: 0.6.0 peberminta: 0.9.0 + parseurl@1.3.3: {} + path-key@3.1.1: {} path-scurry@1.11.1: @@ -2314,6 +3123,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: {} @@ -2358,8 +3174,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 @@ -2375,8 +3204,19 @@ snapshots: filter-obj: 5.1.0 split-on-first: 3.0.0 + querystringify@2.2.0: {} + 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 @@ -2390,8 +3230,15 @@ snapshots: readdirp@4.1.2: {} + requires-port@1.0.0: {} + 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 @@ -2445,22 +3292,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: @@ -2471,6 +3387,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.9.0: {} string-argv@0.3.2: {} @@ -2532,6 +3450,10 @@ snapshots: tinyspy@4.0.3: {} + to-arraybuffer@1.0.1: {} + + toidentifier@1.0.1: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2572,6 +3494,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: {} @@ -2586,10 +3513,25 @@ 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: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + utf8-byte-length@1.0.5: {} + + utils-merge@1.0.1: {} + 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 2826232c..df9efa83 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 bdbbb6a6..43e0d7c3 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 c63f8d6bedfa775519da6fd0a5a900ceba2cf3b7 Mon Sep 17 00:00:00 2001 From: Isabella Aquino Date: Tue, 14 Oct 2025 11:15:46 -0300 Subject: [PATCH 22/25] feat: get html & text columns for broadcasts (#673) --- src/broadcasts/broadcasts.spec.ts | 4 ++++ src/broadcasts/interfaces/broadcast.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index df9efa83..fd6df1b5 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -420,6 +420,7 @@ describe('Broadcasts', () => { name: 'Announcements', audience_id: '78261eea-8f8b-4381-83c6-79fa7120f1cf', from: 'Acme ', + html: '

Hello world

', subject: 'hello world', reply_to: null, preview_text: 'Check out our latest announcements', @@ -427,6 +428,7 @@ describe('Broadcasts', () => { created_at: '2024-12-01T19:32:22.980Z', scheduled_at: null, sent_at: null, + text: 'Hello world', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -447,6 +449,7 @@ describe('Broadcasts', () => { "audience_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf", "created_at": "2024-12-01T19:32:22.980Z", "from": "Acme ", + "html": "

Hello world

", "id": "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", "name": "Announcements", "object": "broadcast", @@ -456,6 +459,7 @@ describe('Broadcasts', () => { "sent_at": null, "status": "draft", "subject": "hello world", + "text": "Hello world", }, "error": null, } diff --git a/src/broadcasts/interfaces/broadcast.ts b/src/broadcasts/interfaces/broadcast.ts index 07cd78ec..c0153441 100644 --- a/src/broadcasts/interfaces/broadcast.ts +++ b/src/broadcasts/interfaces/broadcast.ts @@ -10,4 +10,6 @@ export interface Broadcast { created_at: string; scheduled_at: string | null; sent_at: string | null; + html: string | null; + text: string | null; } From e516568e97726b0587ead20200f9ad7701d89230 Mon Sep 17 00:00:00 2001 From: Isabella Aquino Date: Tue, 14 Oct 2025 13:47:24 -0300 Subject: [PATCH 23/25] feat: bump version (#675) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 889cff1d..2a0cb881 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 a59b055c2bae5fc06da0182f2619435ce512d787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Fri, 17 Oct 2025 14:20:23 -0300 Subject: [PATCH 24/25] chore: sync with canary (#688) Co-authored-by: Isabella Aquino 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: 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 --- package.json | 12 +- pnpm-lock.yaml | 36 + readme.md | 8 +- .../receiving/interfaces/attachment.ts | 3 +- .../interfaces/get-attachment.interface.ts | 14 - .../interfaces/list-attachments.interface.ts | 14 + src/attachments/receiving/receiving.spec.ts | 345 ++++-- src/attachments/receiving/receiving.ts | 4 +- 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 + .../domain-api-options.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-domain-to-api-options.ts | 1 + .../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/domains/domains.spec.ts | 46 + .../create-domain-options.interface.ts | 6 +- src/domains/interfaces/domain.ts | 16 +- .../interfaces/get-domain.interface.ts | 5 +- src/emails/emails.spec.ts | 195 +++- .../create-email-options.interface.ts | 30 +- .../interfaces/get-email-options.interface.ts | 1 + src/interfaces.ts | 1 + src/resend.ts | 6 + 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 ++ src/webhooks/webhooks.spec.ts | 51 + src/webhooks/webhooks.ts | 24 + 73 files changed, 4776 insertions(+), 146 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 create mode 100644 src/webhooks/webhooks.spec.ts create mode 100644 src/webhooks/webhooks.ts diff --git a/package.json b/package.json index 2a0cb881..cbcce849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.0-canary.3", + "version": "6.3.0-canary.1", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -32,19 +32,23 @@ "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" }, "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/resend/resend-node#readme", + "dependencies": { + "svix": "1.76.1" }, - "homepage": "https://github.com/resendlabs/resend-node#readme", "peerDependencies": { "@react-email/render": "*" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e530e68d..74190eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@react-email/render': specifier: '*' version: 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + svix: + specifier: 1.76.1 + version: 1.76.1 devDependencies: '@biomejs/biome': specifier: 2.2.5 @@ -773,6 +776,9 @@ packages: resolution: {integrity: sha512-suq9tRQ6bkpMukTG5K5z0sPWB7t0zExMzZCdmYm6xTSSIm/yCKNm7VCL36wVeyTsFr597/UhU1OAYdHGMDiHrw==} engines: {node: '>=10'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1073,6 +1079,9 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + esbuild@0.25.8: resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} @@ -1107,6 +1116,9 @@ packages: 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==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1655,6 +1667,9 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + svix@1.76.1: + resolution: {integrity: sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1776,6 +1791,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2463,6 +2482,8 @@ snapshots: '@sindresorhus/fnv1a@2.0.1': {} + '@stablelib/base64@1.0.1': {} + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -2730,6 +2751,8 @@ snapshots: dependencies: es-errors: 1.3.0 + es6-promise@4.2.8: {} + esbuild@0.25.8: optionalDependencies: '@esbuild/aix-ppc64': 0.25.8 @@ -2838,6 +2861,8 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-sha256@1.3.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -3427,6 +3452,15 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + svix@1.76.1: + dependencies: + '@stablelib/base64': 1.0.1 + '@types/node': 22.18.9 + es6-promise: 4.2.8 + fast-sha256: 1.3.0 + url-parse: 1.5.10 + uuid: 10.0.0 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -3528,6 +3562,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + validate-npm-package-name@5.0.1: {} vary@1.1.2: {} 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 diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts index 718868a6..e04179ab 100644 --- a/src/attachments/receiving/interfaces/attachment.ts +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -4,5 +4,6 @@ export interface InboundAttachment { content_type: string; content_disposition: 'inline' | 'attachment'; content_id?: string; - content: string; // base64 + 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..4c10e879 100644 --- a/src/attachments/receiving/interfaces/get-attachment.interface.ts +++ b/src/attachments/receiving/interfaces/get-attachment.interface.ts @@ -6,20 +6,6 @@ export interface GetAttachmentOptions { 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; 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..74253f2b 100644 --- a/src/attachments/receiving/receiving.spec.ts +++ b/src/attachments/receiving/receiving.spec.ts @@ -2,8 +2,10 @@ 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(); @@ -48,8 +50,8 @@ describe('Receiving', () => { }); describe('when attachment found', () => { - it('returns attachment with transformed fields', async () => { - const apiResponse: GetAttachmentResponseSuccess = { + it('returns attachment with download URL', async () => { + const apiResponse = { object: 'attachment' as const, data: { id: 'att_123', @@ -57,42 +59,46 @@ 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', + expires_at: '2025-10-18T12:00:00Z', }, }; - 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', + }, }, - }); + ); 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_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', + }, + error: null, + }); }); - it('returns inline attachment', async () => { + it('returns inline attachment with download URL', async () => { const apiResponse = { object: 'attachment' as const, data: { @@ -101,40 +107,43 @@ 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', + expires_at: '2025-10-18T12:00:00Z', }, }; - 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', + }, }, - }); + ); 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_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', + }, + error: null, + }); }); it('handles attachment without optional fields (filename, contentId)', async () => { @@ -145,44 +154,48 @@ describe('Receiving', () => { id: 'att_789', content_type: 'text/plain', content_disposition: 'attachment' as const, - content: 'base64content', + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', // 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', + }, }, - }); + ); 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_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', + }, + error: null, + }); }); }); }); describe('list', () => { - const apiResponse: ListAttachmentsResponseSuccess = { + const apiResponse: ListAttachmentsApiResponse = { object: 'list' as const, has_more: false, data: [ @@ -192,7 +205,8 @@ 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', + expires_at: '2025-10-18T12:00:00Z', }, { id: 'att_456', @@ -200,7 +214,8 @@ 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', + expires_at: '2025-10-18T12:00:00Z', }, ], }; @@ -233,35 +248,69 @@ 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', + 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), + { + 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', }); - 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', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + 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: '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', @@ -281,8 +330,33 @@ describe('Receiving', () => { 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', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + 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: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -294,12 +368,39 @@ describe('Receiving', () => { 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, }); + + 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', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + 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: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -309,12 +410,39 @@ describe('Receiving', () => { 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', }); + + 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', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + 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: apiResponse, + data: expectedResponse, error: null, }); expect(fetchMock.mock.calls[0][0]).toBe( @@ -324,12 +452,39 @@ describe('Receiving', () => { 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', }); + + 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', + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + { + id: 'att_456', + filename: 'image.png', + 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: 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..8a7ab310 100644 --- a/src/attachments/receiving/receiving.ts +++ b/src/attachments/receiving/receiving.ts @@ -6,9 +6,9 @@ import type { GetAttachmentResponseSuccess, } from './interfaces/get-attachment.interface'; import type { + ListAttachmentsApiResponse, ListAttachmentsOptions, ListAttachmentsResponse, - ListAttachmentsResponseSuccess, } from './interfaces/list-attachments.interface'; export class Receiving { @@ -34,7 +34,7 @@ export class Receiving { ? `/emails/receiving/${emailId}/attachments?${queryString}` : `/emails/receiving/${emailId}/attachments`; - const data = await this.resend.get(url); + const data = await this.resend.get(url); return data; } 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 fd6df1b5..c0525eaa 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -428,6 +428,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', }; @@ -460,6 +461,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/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/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-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/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 8786513b..34143473 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, @@ -69,6 +107,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}`, ); @@ -86,6 +131,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}`, { @@ -98,6 +155,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, @@ -108,11 +172,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/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[]; } diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index 43e0d7c3..dec83444 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -64,6 +64,7 @@ describe('Emails', () => { to: 'user@resend.com', subject: 'Not Idempotent Test', html: '

Test

', + topicId: '9f31e56e-3083-46cf-8e96-c6995e0e576a', }; await resend.emails.create(payload); @@ -74,8 +75,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 () => { @@ -390,6 +401,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/interfaces.ts b/src/interfaces.ts index 2d12beef..fab38c00 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -9,6 +9,7 @@ export const RESEND_ERROR_CODES_BY_KEY = { rate_limit_exceeded: 429, missing_api_key: 401, invalid_api_key: 403, + suspended_api_key: 403, invalid_from_address: 403, validation_error: 403, not_found: 404, diff --git a/src/resend.ts b/src/resend.ts index d83ee8f8..413a4c30 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -11,6 +11,9 @@ 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'; const defaultUserAgent = `resend-node:${version}`; @@ -34,6 +37,9 @@ export class Resend { readonly contacts = new Contacts(this); readonly domains = new Domains(this); readonly emails = new Emails(this); + readonly templates = new Templates(this); + readonly topics = new Topics(this); + readonly webhooks = new Webhooks(); 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; + } +} diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts new file mode 100644 index 00000000..62908e43 --- /dev/null +++ b/src/webhooks/webhooks.spec.ts @@ -0,0 +1,51 @@ +import { Webhook } from 'svix'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Webhooks } from './webhooks'; + +const mocks = vi.hoisted(() => { + const verify = vi.fn(); + const webhookConstructor = vi.fn(() => ({ + verify, + })); + + return { + verify, + webhookConstructor, + }; +}); + +vi.mock('svix', () => ({ + Webhook: mocks.webhookConstructor, +})); + +describe('Webhooks', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.verify.mockReset(); + }); + + 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); + + 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); + }); +}); diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts new file mode 100644 index 00000000..41f9f3fa --- /dev/null +++ b/src/webhooks/webhooks.ts @@ -0,0 +1,24 @@ +import { Webhook } from 'svix'; + +interface Headers { + id: string; + timestamp: string; + signature: string; +} + +interface VerifyWebhookOptions { + payload: string; + headers: Headers; + webhookSecret: string; +} + +export class Webhooks { + verify(payload: VerifyWebhookOptions) { + const webhook = new Webhook(payload.webhookSecret); + return webhook.verify(payload.payload, { + 'svix-id': payload.headers.id, + 'svix-timestamp': payload.headers.timestamp, + 'svix-signature': payload.headers.signature, + }); + } +} From 83824136687acfffe877bc01ea22811b0bffe003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Melo?= Date: Fri, 17 Oct 2025 17:32:35 -0300 Subject: [PATCH 25/25] chore: remove template language related aspects (#691) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../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'); }); }); });