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); } 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/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 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 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/integrations/esbuild/.gitignore b/integrations/esbuild/.gitignore new file mode 100644 index 00000000..012a3cd6 --- /dev/null +++ b/integrations/esbuild/.gitignore @@ -0,0 +1 @@ +index.js diff --git a/integrations/esbuild/index.ts b/integrations/esbuild/index.ts new file mode 100644 index 00000000..8b3aa86f --- /dev/null +++ b/integrations/esbuild/index.ts @@ -0,0 +1,3 @@ +import { Resend } from 'resend'; + +new Resend(''); diff --git a/integrations/esbuild/package.json b/integrations/esbuild/package.json new file mode 100644 index 00000000..af7c79bf --- /dev/null +++ b/integrations/esbuild/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "esbuild": "0.25.11", + "resend": "../.." + }, + "scripts": { + "build": "esbuild ./index.ts --bundle --platform=node --target=node18 --outfile=./index.js" + } +} diff --git a/integrations/integrations.spec.ts b/integrations/integrations.spec.ts new file mode 100644 index 00000000..21ef5de7 --- /dev/null +++ b/integrations/integrations.spec.ts @@ -0,0 +1,93 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +describe('integrations', () => { + const sdkPath = path.resolve(__dirname, '..'); + + beforeAll(() => { + const build = spawnSync('pnpm build', { + stdio: 'inherit', + cwd: path.resolve(__dirname, '..'), + shell: true, + }); + if (build.status !== 0) { + throw new Error('SDK build failed'); + } + }); + + /** + * Create an extra temporary copy of the given integration so that there's @react-email/render module resolution from ../node_modules + * + * Also modifies the package.json to point to the SDK with an absolute path. + */ + async function prepareTemporaryIntegrationCopy(integrationPath: string) { + const temporaryIntegrationPath = path.resolve( + os.tmpdir(), + `resend-node-integration-${path.basename(integrationPath)}`, + ); + if (fs.existsSync(temporaryIntegrationPath)) { + await fs.promises.rm(temporaryIntegrationPath, { + recursive: true, + force: true, + }); + } + await fs.promises.mkdir(temporaryIntegrationPath, { recursive: true }); + await fs.promises.cp( + path.resolve(__dirname, integrationPath), + temporaryIntegrationPath, + { + recursive: true, + }, + ); + + const testingLockPackageJson: { dependencies: Record } = + JSON.parse( + await fs.promises.readFile( + path.resolve(temporaryIntegrationPath, 'package.json'), + 'utf8', + ), + ); + testingLockPackageJson.dependencies.resend = sdkPath; + await fs.promises.writeFile( + path.resolve(temporaryIntegrationPath, 'package.json'), + JSON.stringify(testingLockPackageJson, null, 2), + ); + + return temporaryIntegrationPath; + } + + test('nextjs', { timeout: 30_000 }, async () => { + const temporaryNextApp = await prepareTemporaryIntegrationCopy('./nextjs'); + + const buildInstall = spawnSync( + 'npm install --install-links && npm run build', + { + stdio: 'inherit', + cwd: temporaryNextApp, + shell: true, + }, + ); + if (buildInstall.status !== 0) { + throw new Error('next.js build failed'); + } + }); + + test('esbuild', { timeout: 30_000 }, async () => { + const temporaryIntegration = + await prepareTemporaryIntegrationCopy('./esbuild'); + + const buildInstall = spawnSync( + 'npm install --install-links && npm run build', + { + stdio: 'inherit', + cwd: temporaryIntegration, + shell: true, + }, + ); + if (buildInstall.status !== 0) { + throw new Error('next.js build failed'); + } + }); +}); diff --git a/integrations/nextjs/.gitignore b/integrations/nextjs/.gitignore new file mode 100644 index 00000000..c6e1e1f4 --- /dev/null +++ b/integrations/nextjs/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +package-lock.json +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/integrations/nextjs/app/route.js b/integrations/nextjs/app/route.js new file mode 100644 index 00000000..b2e5a9dc --- /dev/null +++ b/integrations/nextjs/app/route.js @@ -0,0 +1,7 @@ +import { Resend } from 'resend'; + +export function GET() { + new Resend(''); + + return new Response('Hello from this API route!', { status: 200 }); +} diff --git a/integrations/nextjs/next.config.js b/integrations/nextjs/next.config.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/integrations/nextjs/next.config.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/integrations/nextjs/package.json b/integrations/nextjs/package.json new file mode 100644 index 00000000..c76d59fa --- /dev/null +++ b/integrations/nextjs/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "next": "15.5.6", + "react": "19.2.0", + "react-dom": "19.2.0", + "resend": "../.." + }, + "scripts": { + "build": "next build" + } +} diff --git a/package.json b/package.json index 7f40d664..701de618 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.2.2", + "version": "6.3.0-canary.1", "description": "Node.js library for the Resend API", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -28,21 +28,26 @@ "format": "biome format --write .", "format:apply": "biome check --write .", "format:check": "biome format .", + "integration:nextjs": "cd ./integrations/nextjs && next build --turbopack", "lint": "biome check .", "prepublishOnly": "pnpm run build", "test": "vitest run", - "test:watch": "vitest" + "test:dev": "cross-env TEST_MODE=dev vitest run", + "test:integrations": "vitest run integrations", + "test:record": "rimraf --glob \"**/__recordings__\" && cross-env TEST_MODE=record vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" }, "repository": { "type": "git", - "url": "git+https://github.com/resendlabs/resend-node.git" + "url": "git+https://github.com/resend/resend-node.git" }, "author": "", "license": "MIT", "bugs": { - "url": "https://github.com/resendlabs/resend-node/issues" + "url": "https://github.com/resend/resend-node/issues" }, - "homepage": "https://github.com/resendlabs/resend-node#readme", + "homepage": "https://github.com/resend/resend-node#readme", "dependencies": { "svix": "1.76.1" }, @@ -55,14 +60,20 @@ } }, "devDependencies": { - "@biomejs/biome": "2.2.0", - "@types/node": "22.18.6", - "@types/react": "19.1.15", + "@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.2", + "typescript": "5.9.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5" }, - "packageManager": "pnpm@10.17.1" + "packageManager": "pnpm@10.18.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b26e5b7f..74190eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,29 +16,47 @@ importers: version: 1.76.1 devDependencies: '@biomejs/biome': - specifier: 2.2.0 - version: 2.2.0 + 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.6 - version: 22.18.6 + specifier: 22.18.9 + version: 22.18.9 '@types/react': - specifier: 19.1.15 - version: 19.1.15 + 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.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.6)(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.6)(yaml@2.8.1)) + version: 0.4.5(vitest@3.2.4(@types/node@22.18.9)(yaml@2.8.1)) packages: @@ -54,59 +72,62 @@ 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.5': + resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==} 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.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.0': - resolution: {integrity: sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==} + '@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.0': - resolution: {integrity: sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==} + '@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.0': - resolution: {integrity: sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==} + '@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.0': - resolution: {integrity: sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==} + '@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.0': - resolution: {integrity: sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==} + '@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.0': - resolution: {integrity: sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==} + '@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.0': - resolution: {integrity: sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==} + '@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] + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} @@ -423,6 +444,14 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -507,6 +536,27 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pollyjs/adapter-fetch@6.0.7': + resolution: {integrity: sha512-kv44DROx/2qzlcgS71EccGr2/I5nK40Xt92paGNI+1/Kmz290bw/ykt8cvXDg4O4xCc9Fh/jXeAkS7qwGpCx2g==} + + '@pollyjs/adapter@6.0.6': + resolution: {integrity: sha512-szhys0NiFQqCJDMC0kpDyjhLqSI7aWc6m6iATCRKgcMcN/7QN85pb3GmRzvnNV8+/Bi2AUSCwxZljcsKhbYVWQ==} + + '@pollyjs/core@6.0.6': + resolution: {integrity: sha512-1ZZcmojW8iSFmvHGeLlvuudM3WiDV842FsVvtPAo3HoAYE6jCNveLHJ+X4qvonL4enj1SyTF3hXA107UkQFQrA==} + + '@pollyjs/node-server@6.0.6': + resolution: {integrity: sha512-nkP1+hdNoVOlrRz9R84haXVsaSmo8Xmq7uYK9GeUMSLQy4Fs55ZZ9o2KI6vRA8F6ZqJSbC31xxwwIoTkjyP7Vg==} + + '@pollyjs/persister-fs@6.0.6': + resolution: {integrity: sha512-/ALVgZiH2zGqwLkW0Mntc0Oq1v7tR8LS8JD2SAyIsHpnSXeBUnfPWwjAuYw0vqORHFVEbwned6MBRFfvU/3qng==} + + '@pollyjs/persister@6.0.6': + resolution: {integrity: sha512-9KB1p+frvYvFGur4ifzLnFKFLXAMXrhAhCnVhTnkG2WIqqQPT7y+mKBV/DKCmYFx8GPA9FiNGqt2pB53uJpIdw==} + + '@pollyjs/utils@6.0.6': + resolution: {integrity: sha512-nhVJoI3nRgRimE0V2DVSvsXXNROUH6iyJbroDu4IdsOIOFC1Ds0w+ANMB4NMwFaqE+AisWOmXFzwAGdAfyiQVg==} + '@react-email/render@1.2.3': resolution: {integrity: sha512-qu3XYNkHGao3teJexVD5CrcgFkNLrzbZvpZN17a7EyQYUN3kHkTkE9saqY4VbvGx6QoNU3p8rsk/Xm++D/+pTw==} engines: {node: '>=18.0.0'} @@ -722,6 +772,10 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sindresorhus/fnv1a@2.0.1': + resolution: {integrity: sha512-suq9tRQ6bkpMukTG5K5z0sPWB7t0zExMzZCdmYm6xTSSIm/yCKNm7VCL36wVeyTsFr597/UhU1OAYdHGMDiHrw==} + engines: {node: '>=10'} + '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -734,11 +788,14 @@ 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.9': + resolution: {integrity: sha512-5yBtK0k/q8PjkMXbTfeIEP/XVYnz1R9qZJ3yUicdEW7ppdDJfe+MqXEhpqDL3mtn4Wvs1u0KLEG0RXzCgNpsSg==} + + '@types/react@19.2.0': + resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} - '@types/react@19.1.15': - resolution: {integrity: sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -769,6 +826,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -793,6 +854,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -800,9 +864,23 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -812,10 +890,22 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -849,6 +939,30 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -856,6 +970,14 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -886,9 +1008,17 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -902,22 +1032,53 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -931,16 +1092,30 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -957,6 +1132,10 @@ packages: resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} engines: {node: '>=14.16'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -964,15 +1143,58 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -980,10 +1202,33 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-graceful-shutdown@3.1.14: + resolution: {integrity: sha512-aTbGAZDUtRt7gRmU+li7rt5WbJeemULZHLNrycJ1dRBU80Giut6NvzG8h5u1TW1zGHXkPGpEtoEKhPKogIRKdA==} + engines: {node: '>=4.0.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-absolute-url@3.0.3: + resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==} + engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -998,6 +1243,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1005,6 +1254,9 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -1019,21 +1271,64 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1048,6 +1343,13 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1059,10 +1361,34 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + nocache@3.0.4: + resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} + engines: {node: '>=12.0.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1072,6 +1398,10 @@ packages: parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1080,6 +1410,13 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1135,10 +1472,22 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + query-registry@3.0.1: resolution: {integrity: sha512-M9RxRITi2mHMVPU5zysNjctUT8bAPx6ltEXo/ir9+qmiM47Y7f0Ir3+OxUO5OjYAWdicBQRew7RtHtqUXydqlg==} engines: {node: '>=20'} @@ -1154,6 +1503,14 @@ packages: resolution: {integrity: sha512-Pzd/4IFnTb8E+I1P5rbLQoqpUHcXKg48qTYKi4EANg+sTPwGFEMOcYGiiZz6xuQcOMZP7MPsrdAPx+16Q8qahg==} engines: {node: '>=18'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -1177,6 +1534,11 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + rollup@4.46.2: resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1187,12 +1549,38 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + route-recognizer@0.3.4: + resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1201,6 +1589,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1208,6 +1612,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1224,6 +1632,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -1287,6 +1699,13 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1324,8 +1743,12 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + 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'} hasBin: true @@ -1346,6 +1769,14 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + url-join@5.0.0: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1353,6 +1784,13 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -1361,6 +1799,10 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1537,41 +1979,43 @@ snapshots: '@actions/io@1.1.3': {} - '@biomejs/biome@2.2.0': + '@biomejs/biome@2.2.5': 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.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.0': + '@biomejs/cli-darwin-arm64@2.2.5': optional: true - '@biomejs/cli-darwin-x64@2.2.0': + '@biomejs/cli-darwin-x64@2.2.5': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.0': + '@biomejs/cli-linux-arm64-musl@2.2.5': optional: true - '@biomejs/cli-linux-arm64@2.2.0': + '@biomejs/cli-linux-arm64@2.2.5': optional: true - '@biomejs/cli-linux-x64-musl@2.2.0': + '@biomejs/cli-linux-x64-musl@2.2.5': optional: true - '@biomejs/cli-linux-x64@2.2.0': + '@biomejs/cli-linux-x64@2.2.5': optional: true - '@biomejs/cli-win32-arm64@2.2.0': + '@biomejs/cli-win32-arm64@2.2.5': optional: true - '@biomejs/cli-win32-x64@2.2.0': + '@biomejs/cli-win32-x64@2.2.5': optional: true + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.25.8': optional: true @@ -1730,6 +2174,12 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1837,6 +2287,63 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pollyjs/adapter-fetch@6.0.7': + dependencies: + '@pollyjs/adapter': 6.0.6 + '@pollyjs/utils': 6.0.6 + to-arraybuffer: 1.0.1 + + '@pollyjs/adapter@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + + '@pollyjs/core@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + '@sindresorhus/fnv1a': 2.0.1 + blueimp-md5: 2.19.0 + fast-json-stable-stringify: 2.1.0 + is-absolute-url: 3.0.3 + lodash-es: 4.17.21 + loglevel: 1.9.2 + route-recognizer: 0.3.4 + slugify: 1.6.6 + + '@pollyjs/node-server@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + body-parser: 1.20.3 + cors: 2.8.5 + express: 4.21.2 + fs-extra: 10.1.0 + http-graceful-shutdown: 3.1.14 + morgan: 1.10.1 + nocache: 3.0.4 + transitivePeerDependencies: + - supports-color + + '@pollyjs/persister-fs@6.0.6': + dependencies: + '@pollyjs/node-server': 6.0.6 + '@pollyjs/persister': 6.0.6 + transitivePeerDependencies: + - supports-color + + '@pollyjs/persister@6.0.6': + dependencies: + '@pollyjs/utils': 6.0.6 + '@types/set-cookie-parser': 2.4.10 + bowser: 2.12.1 + fast-json-stable-stringify: 2.1.0 + lodash-es: 4.17.21 + set-cookie-parser: 2.7.1 + utf8-byte-length: 1.0.5 + + '@pollyjs/utils@6.0.6': + dependencies: + qs: 6.14.0 + url-parse: 1.5.10 + '@react-email/render@1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: html-to-text: 9.0.5 @@ -1973,6 +2480,8 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sindresorhus/fnv1a@2.0.1': {} + '@stablelib/base64@1.0.1': {} '@types/chai@5.2.2': @@ -1983,14 +2492,18 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@22.18.6': + '@types/node@22.18.9': dependencies: undici-types: 6.21.0 - '@types/react@19.1.15': + '@types/react@19.2.0': 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 @@ -1999,13 +2512,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.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.6)(yaml@2.8.1) + vite: 7.1.1(@types/node@22.18.9)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2033,6 +2546,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn@8.15.0: {} ansi-regex@5.0.1: {} @@ -2047,12 +2565,39 @@ snapshots: any-promise@1.3.0: {} + array-flatten@1.1.1: {} + assertion-error@2.0.1: {} balanced-match@1.0.2: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + before-after-hook@2.2.3: {} + blueimp-md5@2.19.0: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bowser@2.12.1: {} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2062,8 +2607,20 @@ snapshots: esbuild: 0.25.8 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} chai@5.3.3: @@ -2092,6 +2649,26 @@ snapshots: consola@3.4.2: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2100,6 +2677,10 @@ snapshots: csstype@3.1.3: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2114,8 +2695,12 @@ snapshots: deepmerge@4.3.1: {} + depd@2.0.0: {} + deprecation@2.3.1: {} + destroy@1.2.0: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -2134,16 +2719,38 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + entities@4.5.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es6-promise@4.2.8: {} esbuild@0.25.8: @@ -2204,14 +2811,56 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + expect-type@1.2.2: {} + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@2.0.1: {} + fast-json-stable-stringify@2.1.0: {} + fast-sha256@1.3.0: {} fdir@6.5.0(picomatch@4.0.3): @@ -2220,6 +2869,18 @@ snapshots: filter-obj@5.1.0: {} + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.17 @@ -2231,9 +2892,39 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -2243,6 +2934,25 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -2258,8 +2968,32 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-graceful-shutdown@3.1.14: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-absolute-url@3.0.3: {} + is-fullwidth-code-point@3.0.0: {} isbinaryfile@5.0.6: {} @@ -2272,10 +3006,20 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + joycon@3.1.1: {} js-tokens@9.0.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + leac@0.6.0: {} lilconfig@3.1.3: {} @@ -2284,12 +3028,18 @@ snapshots: load-tsconfig@0.2.5: {} + lodash-es@4.17.21: {} + lodash.sortby@4.7.0: {} + loglevel@1.9.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 @@ -2298,6 +3048,26 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -2318,6 +3088,18 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + ms@2.1.3: {} mz@2.7.0: @@ -2328,8 +3110,24 @@ snapshots: nanoid@3.3.11: {} + negotiator@0.6.3: {} + + nocache@3.0.4: {} + object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -2341,6 +3139,8 @@ snapshots: leac: 0.6.0 peberminta: 0.9.0 + parseurl@1.3.3: {} + path-key@3.1.1: {} path-scurry@1.11.1: @@ -2348,6 +3148,13 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2392,8 +3199,21 @@ snapshots: prettier@3.6.2: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + query-registry@3.0.1: dependencies: query-string: 9.3.0 @@ -2413,6 +3233,15 @@ snapshots: quick-lru@7.1.0: {} + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -2430,6 +3259,11 @@ snapshots: resolve-from@5.0.0: {} + rimraf@6.0.1: + dependencies: + glob: 11.0.3 + package-json-from-dist: 1.0.1 + rollup@4.46.2: dependencies: '@types/estree': 1.0.8 @@ -2483,22 +3317,91 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.50.2 fsevents: 2.3.3 + route-recognizer@0.3.4: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + scheduler@0.26.0: {} selderee@0.11.0: dependencies: parseley: 0.12.1 + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.1: {} + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} + slugify@1.6.6: {} + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -2509,6 +3412,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.9.0: {} string-argv@0.3.2: {} @@ -2550,7 +3455,7 @@ snapshots: svix@1.76.1: dependencies: '@stablelib/base64': 1.0.1 - '@types/node': 22.18.6 + '@types/node': 22.18.9 es6-promise: 4.2.8 fast-sha256: 1.3.0 url-parse: 1.5.10 @@ -2579,6 +3484,10 @@ snapshots: tinyspy@4.0.3: {} + to-arraybuffer@1.0.1: {} + + toidentifier@1.0.1: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2587,7 +3496,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1): + tsup@8.5.0(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.8) cac: 6.7.14 @@ -2608,7 +3517,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.6 - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color @@ -2619,7 +3528,12 @@ snapshots: type-detect@4.1.0: {} - typescript@5.9.2: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.9.3: {} ufo@1.6.1: {} @@ -2633,6 +3547,10 @@ snapshots: universal-user-agent@6.0.1: {} + universalify@2.0.1: {} + + unpipe@1.0.0: {} + url-join@5.0.0: {} url-parse@1.5.10: @@ -2640,17 +3558,23 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + utf8-byte-length@1.0.5: {} + + utils-merge@1.0.1: {} + uuid@10.0.0: {} validate-npm-package-name@5.0.1: {} - vite-node@3.2.4(@types/node@22.18.6)(yaml@2.8.1): + vary@1.1.2: {} + + 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.6)(yaml@2.8.1) + vite: 7.1.5(@types/node@22.18.9)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2665,7 +3589,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.9)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2674,11 +3598,11 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 22.18.9 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.9)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2687,19 +3611,19 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@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.6)(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.6)(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.6)(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.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@22.18.9)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2717,11 +3641,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.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.6 + '@types/node': 22.18.9 transitivePeerDependencies: - jiti - less 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/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/attachments.ts b/src/attachments/attachments.ts new file mode 100644 index 00000000..80e48f52 --- /dev/null +++ b/src/attachments/attachments.ts @@ -0,0 +1,10 @@ +import type { Resend } from '../resend'; +import { Receiving } from './receiving/receiving'; + +export class Attachments { + readonly receiving: Receiving; + + constructor(resend: Resend) { + this.receiving = new Receiving(resend); + } +} diff --git a/src/attachments/receiving/interfaces/attachment.ts b/src/attachments/receiving/interfaces/attachment.ts new file mode 100644 index 00000000..e04179ab --- /dev/null +++ b/src/attachments/receiving/interfaces/attachment.ts @@ -0,0 +1,9 @@ +export interface InboundAttachment { + id: string; + filename?: string; + content_type: string; + content_disposition: 'inline' | 'attachment'; + content_id?: string; + download_url: string; + expires_at: string; +} diff --git a/src/attachments/receiving/interfaces/get-attachment.interface.ts b/src/attachments/receiving/interfaces/get-attachment.interface.ts new file mode 100644 index 00000000..4c10e879 --- /dev/null +++ b/src/attachments/receiving/interfaces/get-attachment.interface.ts @@ -0,0 +1,22 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { InboundAttachment } from './attachment'; + +export interface GetAttachmentOptions { + emailId: string; + id: string; +} + +export interface GetAttachmentResponseSuccess { + object: 'attachment'; + data: InboundAttachment; +} + +export type GetAttachmentResponse = + | { + data: GetAttachmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/attachments/receiving/interfaces/index.ts b/src/attachments/receiving/interfaces/index.ts new file mode 100644 index 00000000..ec3f8400 --- /dev/null +++ b/src/attachments/receiving/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/receiving/interfaces/list-attachments.interface.ts b/src/attachments/receiving/interfaces/list-attachments.interface.ts new file mode 100644 index 00000000..3f174645 --- /dev/null +++ b/src/attachments/receiving/interfaces/list-attachments.interface.ts @@ -0,0 +1,37 @@ +import type { PaginationOptions } from '../../../common/interfaces'; +import type { ErrorResponse } from '../../../interfaces'; +import type { InboundAttachment } from './attachment'; + +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; + data: InboundAttachment[]; +} + +export type ListAttachmentsResponse = + | { + data: ListAttachmentsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/attachments/receiving/receiving.spec.ts b/src/attachments/receiving/receiving.spec.ts new file mode 100644 index 00000000..74253f2b --- /dev/null +++ b/src/attachments/receiving/receiving.spec.ts @@ -0,0 +1,496 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../../interfaces'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + ListAttachmentsApiResponse, + ListAttachmentsResponseSuccess, +} from './interfaces/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', () => { + 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.receiving.get({ + emailId: '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 download URL', async () => { + const apiResponse = { + object: 'attachment' as const, + data: { + id: 'att_123', + filename: 'document.pdf', + content_type: 'application/pdf', + content_id: 'cid_123', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_123', + expires_at: '2025-10-18T12:00:00Z', + }, + }; + + 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).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 with download URL', async () => { + const apiResponse = { + object: 'attachment' as const, + data: { + id: 'att_456', + filename: 'image.png', + content_type: 'image/png', + content_id: 'cid_456', + content_disposition: 'inline' as const, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + }; + + 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).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 () => { + const apiResponse = { + object: 'attachment' as const, + data: { + // Required fields based on DB schema + id: 'att_789', + content_type: 'text/plain', + content_disposition: 'attachment' as const, + download_url: 'https://example.com/download/att_789', + expires_at: '2025-10-18T12:00:00Z', + // Optional fields (filename, content_id) omitted + }, + }; + + 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).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: ListAttachmentsApiResponse = { + 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, + 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' as const, + download_url: 'https://example.com/download/att_456', + expires_at: '2025-10-18T12:00:00Z', + }, + ], + }; + + const headers = { + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }; + + describe('when inbound email not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: '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.receiving.list({ + emailId: '61cda979-919d-4b9d-9638-c148b93ff410', + }); + + expect(result).toEqual({ data: null, error: response }); + }); + }); + + describe('when attachments found', () => { + it('returns multiple attachments with download URLs', async () => { + fetchMock.mockOnceIf( + 'https://api.resend.com/emails/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', + }); + + 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: 'list' as const, + has_more: false, + data: [], + }; + + 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', + }); + + 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', + }); + + 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, + }); + 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, + }); + + 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, + }); + 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', + }); + + 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, + }); + 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', + }); + + 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, + }); + 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 new file mode 100644 index 00000000..8a7ab310 --- /dev/null +++ b/src/attachments/receiving/receiving.ts @@ -0,0 +1,41 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + GetAttachmentOptions, + GetAttachmentResponse, + GetAttachmentResponseSuccess, +} from './interfaces/get-attachment.interface'; +import type { + ListAttachmentsApiResponse, + ListAttachmentsOptions, + ListAttachmentsResponse, +} 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/receiving/${emailId}/attachments/${id}`, + ); + + return data; + } + + async list( + options: ListAttachmentsOptions, + ): Promise { + const { emailId } = options; + + 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/audiences/audiences.ts b/src/audiences/audiences.ts deleted file mode 100644 index ae0427b3..00000000 --- a/src/audiences/audiences.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import type { Resend } from '../resend'; -import type { - CreateAudienceOptions, - CreateAudienceRequestOptions, - CreateAudienceResponse, - CreateAudienceResponseSuccess, -} from './interfaces/create-audience-options.interface'; -import type { - GetAudienceResponse, - GetAudienceResponseSuccess, -} from './interfaces/get-audience.interface'; -import type { - ListAudiencesOptions, - ListAudiencesResponse, - ListAudiencesResponseSuccess, -} from './interfaces/list-audiences.interface'; -import type { - RemoveAudiencesResponse, - RemoveAudiencesResponseSuccess, -} from './interfaces/remove-audience.interface'; - -export class Audiences { - constructor(private readonly resend: Resend) {} - - async create( - payload: CreateAudienceOptions, - options: CreateAudienceRequestOptions = {}, - ): Promise { - const data = await this.resend.post( - '/audiences', - payload, - options, - ); - return data; - } - - async list( - options: ListAudiencesOptions = {}, - ): Promise { - const queryString = buildPaginationQuery(options); - const url = queryString ? `/audiences?${queryString}` : '/audiences'; - - const data = await this.resend.get(url); - return data; - } - - async get(id: string): Promise { - const data = await this.resend.get( - `/audiences/${id}`, - ); - return data; - } - - async remove(id: string): Promise { - const data = await this.resend.delete( - `/audiences/${id}`, - ); - return data; - } -} diff --git a/src/audiences/interfaces/create-audience-options.interface.ts b/src/audiences/interfaces/create-audience-options.interface.ts deleted file mode 100644 index f8775dba..00000000 --- a/src/audiences/interfaces/create-audience-options.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { PostOptions } from '../../common/interfaces'; -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface CreateAudienceOptions { - name: string; -} - -export interface CreateAudienceRequestOptions extends PostOptions {} - -export interface CreateAudienceResponseSuccess - extends Pick { - object: 'audience'; -} - -export type CreateAudienceResponse = - | { - data: CreateAudienceResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/audiences/interfaces/get-audience.interface.ts b/src/audiences/interfaces/get-audience.interface.ts deleted file mode 100644 index 9c08efac..00000000 --- a/src/audiences/interfaces/get-audience.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface GetAudienceResponseSuccess - extends Pick { - object: 'audience'; -} - -export type GetAudienceResponse = - | { - data: GetAudienceResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/audiences/interfaces/index.ts b/src/audiences/interfaces/index.ts deleted file mode 100644 index 0aecd9db..00000000 --- a/src/audiences/interfaces/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './audience'; -export * from './create-audience-options.interface'; -export * from './get-audience.interface'; -export * from './list-audiences.interface'; -export * from './remove-audience.interface'; diff --git a/src/audiences/interfaces/remove-audience.interface.ts b/src/audiences/interfaces/remove-audience.interface.ts deleted file mode 100644 index e82e0b39..00000000 --- a/src/audiences/interfaces/remove-audience.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; - -export interface RemoveAudiencesResponseSuccess extends Pick { - object: 'audience'; - deleted: boolean; -} - -export type RemoveAudiencesResponse = - | { - data: RemoveAudiencesResponseSuccess; - error: null; - } - | { - data: null; - error: ErrorResponse; - }; diff --git a/src/batch/batch.spec.ts b/src/batch/batch.spec.ts index 219dc662..396235a1 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 () => { @@ -365,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 feca37ed..605f30dc 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 () => { @@ -58,7 +63,7 @@ describe('Broadcasts', () => { const payload: CreateBroadcastOptions = { from: 'bu@resend.com', - audienceId: '0192f4ed-c2e9-7112-9c13-b04a043e23ee', + segmentId: '0192f4ed-c2e9-7112-9c13-b04a043e23ee', subject: 'Hello World', html: '

Hello world

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

Hello world

', @@ -431,6 +439,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', }; @@ -459,10 +468,12 @@ describe('Broadcasts', () => { "preview_text": "Check out our latest announcements", "reply_to": null, "scheduled_at": null, + "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf", "sent_at": null, "status": "draft", "subject": "hello world", "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..2c383a2b 100644 --- a/src/broadcasts/broadcasts.ts +++ b/src/broadcasts/broadcasts.ts @@ -44,6 +44,7 @@ export class Broadcasts { '/broadcasts', { name: payload.name, + segment_id: payload.segmentId, audience_id: payload.audienceId, preview_text: payload.previewText, from: payload.from, @@ -51,6 +52,7 @@ export class Broadcasts { reply_to: payload.replyTo, subject: payload.subject, text: payload.text, + topic_id: payload.topicId, }, options, ); @@ -106,6 +108,7 @@ export class Broadcasts { `/broadcasts/${id}`, { name: payload.name, + segment_id: payload.segmentId, audience_id: payload.audienceId, from: payload.from, html: payload.html, @@ -113,6 +116,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..b8b49718 100644 --- a/src/broadcasts/interfaces/broadcast.ts +++ b/src/broadcasts/interfaces/broadcast.ts @@ -1,6 +1,7 @@ export interface Broadcast { id: string; name: string; + segment_id: string | null; audience_id: string | null; from: string | null; subject: string | null; @@ -10,6 +11,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..38fb6bfa 100644 --- a/src/broadcasts/interfaces/create-broadcast-options.interface.ts +++ b/src/broadcasts/interfaces/create-broadcast-options.interface.ts @@ -24,19 +24,26 @@ interface EmailRenderOptions { text: string; } -interface CreateBroadcastBaseOptions { +interface SegmentOptions { /** - * The name of the broadcast + * The id of the segment you want to send to * * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters */ - name?: string; + segmentId: string; + /** + * @deprecated Use segmentId instead + */ + audienceId: string; +} + +interface CreateBroadcastBaseOptions { /** - * The id of the audience you want to send to + * The name of the broadcast * * @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters */ - audienceId: string; + name?: string; /** * A short snippet of text displayed as a preview in recipients' inboxes, often shown below or beside the subject line. * @@ -61,9 +68,16 @@ 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 & + RequireAtLeastOne & CreateBroadcastBaseOptions; export interface CreateBroadcastRequestOptions extends PostOptions {} diff --git a/src/broadcasts/interfaces/list-broadcasts.interface.ts b/src/broadcasts/interfaces/list-broadcasts.interface.ts index 8061427b..c600cc5d 100644 --- a/src/broadcasts/interfaces/list-broadcasts.interface.ts +++ b/src/broadcasts/interfaces/list-broadcasts.interface.ts @@ -12,6 +12,7 @@ export type ListBroadcastsResponseSuccess = { | 'id' | 'name' | 'audience_id' + | 'segment_id' | 'status' | 'created_at' | 'scheduled_at' diff --git a/src/broadcasts/interfaces/update-broadcast.interface.ts b/src/broadcasts/interfaces/update-broadcast.interface.ts index fa776dd3..bc78728a 100644 --- a/src/broadcasts/interfaces/update-broadcast.interface.ts +++ b/src/broadcasts/interfaces/update-broadcast.interface.ts @@ -6,6 +6,10 @@ export interface UpdateBroadcastResponseSuccess { export type UpdateBroadcastOptions = { name?: string; + segmentId?: string; + /** + * @deprecated Use segmentId instead + */ audienceId?: string; from?: string; html?: string; @@ -14,6 +18,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-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..173b5068 --- /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": "665547b9aa413b822892a55e1667ce49", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for contact creation\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 109, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 109, + "text": "{\"object\":\"audience\",\"id\":\"342faa8f-995c-4140-a424-737be8d1ced5\",\"name\":\"Test audience for contact creation\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99017480f839ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "109" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:25 GMT" + }, + { + "name": "etag", + "value": "W/\"6d-jyZnjym7Jd2ZRJlgVDDG3P7hS6Q\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:25.333Z", + "time": 257, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 257 + } + }, + { + "_id": "d510199548e03edeef157758fb721017", + "_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.3.0-canary.0" + } + ], + "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/342faa8f-995c-4140-a424-737be8d1ced5/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"b7da9948-0951-43d4-88a2-69f336494fed\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174826e775913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:25 GMT" + }, + { + "name": "etag", + "value": "W/\"40-0YBQiM/UDsdlMPisb0sTJTN1dLI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:25.592Z", + "time": 407, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 407 + } + }, + { + "_id": "80ce90fc77fe48120b255542f1fe1e29", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/342faa8f-995c-4140-a424-737be8d1ced5" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"342faa8f-995c-4140-a424-737be8d1ced5\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174849cc5ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:26 GMT" + }, + { + "name": "etag", + "value": "W/\"50-ZLUepDaD1nIG5Xzqbw8ICWRnMAM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:26.000Z", + "time": 314, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 314 + } + } + ], + "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..bf31a698 --- /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": "7e36d93bf833d78073dab4d08fb175ea", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/contacts" + }, + "response": { + "bodySize": 85, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 85, + "text": "{\"statusCode\":422,\"message\":\"Missing `email` field.\",\"name\":\"missing_required_field\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174869c35ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "85" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:26 GMT" + }, + { + "name": "etag", + "value": "W/\"55-3twEi8WnC2GKqaYdAl7cGsmeByc\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-17T17:18:26.318Z", + "time": 154, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 154 + } + } + ], + "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..26102539 --- /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": "a7148d90c9796babcc02e83f1bafa2fb", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 105, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 105, + "text": "{\"object\":\"audience\",\"id\":\"b12a7fd8-7514-4893-8361-9ef21678dc4c\",\"name\":\"Test audience for get by email\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174aa8d29ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "105" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:32 GMT" + }, + { + "name": "etag", + "value": "W/\"69-mYlmY131iqLPmNWxIqBOUbAO/GU\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:32.073Z", + "time": 189, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 189 + } + }, + { + "_id": "3258d18c3a0b32f88d3cb554fe5cce91", + "_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.3.0-canary.0" + } + ], + "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/b12a7fd8-7514-4893-8361-9ef21678dc4c/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ecfa803f-113f-4d65-b506-5885d692a21b\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174abccd65913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:32 GMT" + }, + { + "name": "etag", + "value": "W/\"40-Oz0WE77GhPhjj1/hpRMX+ZT3mOo\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:32.263Z", + "time": 295, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 295 + } + }, + { + "_id": "c5dcc049ae32086e3e156d07fbf9885d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 257, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/b12a7fd8-7514-4893-8361-9ef21678dc4c/contacts/test-get-by-email@example.com" + }, + "response": { + "bodySize": 205, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 205, + "text": "{\"object\":\"contact\",\"id\":\"ecfa803f-113f-4d65-b506-5885d692a21b\",\"email\":\"test-get-by-email@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:14.626031+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174ad9831ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:32 GMT" + }, + { + "name": "etag", + "value": "W/\"cd-IJhBNDZfJtgUskPzfvmcAOXC03Q\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:32.559Z", + "time": 188, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 188 + } + }, + { + "_id": "7e0a943177b9b8aa08d301f9324a8d0e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/b12a7fd8-7514-4893-8361-9ef21678dc4c" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"b12a7fd8-7514-4893-8361-9ef21678dc4c\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174aebc6dad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:32 GMT" + }, + { + "name": "etag", + "value": "W/\"50-myNtkcJEINsG49X2F2FiXGw1MFY\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:32.747Z", + "time": 324, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 324 + } + } + ], + "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..5c06463c --- /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": "ed88a0d65efc2ef06d7d97751b5d6c79", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 102, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 102, + "text": "{\"object\":\"audience\",\"id\":\"8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d\",\"name\":\"Test audience for get by ID\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a48e8bad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "102" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:31 GMT" + }, + { + "name": "etag", + "value": "W/\"66-kY9l4F7yZU4E6WejINms8RmFE/Q\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:31.110Z", + "time": 156, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 156 + } + }, + { + "_id": "4b5fa999d8c434c106162f448bafe147", + "_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.3.0-canary.0" + } + ], + "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/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"5e26e74a-65db-452e-a6b6-a4fa4dd60afe\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a588f55913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:31 GMT" + }, + { + "name": "etag", + "value": "W/\"40-2A3wqCIyroDRBQYGkrJpkl8H/rE\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:31.268Z", + "time": 264, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 264 + } + }, + { + "_id": "e81aad142891d3dfbdbbf7b647cbb497", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d/contacts/5e26e74a-65db-452e-a6b6-a4fa4dd60afe" + }, + "response": { + "bodySize": 202, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 202, + "text": "{\"object\":\"contact\",\"id\":\"5e26e74a-65db-452e-a6b6-a4fa4dd60afe\",\"email\":\"test-get-by-id@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:12.007797+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a7292cad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:31 GMT" + }, + { + "name": "etag", + "value": "W/\"ca-bL+s+mrU1ZdXrP0HGVy1euucMjc\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:31.534Z", + "time": 213, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 213 + } + }, + { + "_id": "3f937b654e55ffbee0afb085b440a1a0", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"8ad0a4bc-897b-4dc6-9cb6-7e52f5cc163d\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a88df9ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:31 GMT" + }, + { + "name": "etag", + "value": "W/\"50-ja5KH7a2OoHKanEhx4zGIynzfIs\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:31.748Z", + "time": 318, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 318 + } + } + ], + "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..7c0ee293 --- /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": "efdfa668e0efb9cb39867c45bc2b2f4f", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 113, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 113, + "text": "{\"object\":\"audience\",\"id\":\"f9f4a08d-51c6-485a-b89a-97ca2d999ee5\",\"name\":\"Test audience for non-existent contact\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174b0cba3ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "113" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:33 GMT" + }, + { + "name": "etag", + "value": "W/\"71-KcnX6K2x2k8mhaatY2X239GxLFI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:33.075Z", + "time": 231, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 231 + } + }, + { + "_id": "8c2ea5af757170ac609bbc43cfa6e26e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/f9f4a08d-51c6-485a-b89a-97ca2d999ee5/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": "990174b2ba6a5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:33 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-17T17:18:33.308Z", + "time": 296, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 296 + } + }, + { + "_id": "2aa04ee6117a8415f441929306418d5f", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/f9f4a08d-51c6-485a-b89a-97ca2d999ee5" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"f9f4a08d-51c6-485a-b89a-97ca2d999ee5\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174b41ef4ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:33 GMT" + }, + { + "name": "etag", + "value": "W/\"50-XNsH+fU0j58WAyffrRj19tfwD4w\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:33.605Z", + "time": 426, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 426 + } + } + ], + "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..51056346 --- /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": "27dc1ad8de477b198c1d2b9050b7fe03", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 111, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 111, + "text": "{\"object\":\"audience\",\"id\":\"59e81eca-46ce-44d8-a8c7-6eb2c2350f30\",\"name\":\"Test audience for listing with limit\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174958f8cad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "111" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:28 GMT" + }, + { + "name": "etag", + "value": "W/\"6f-nj0fLhvMB7bgJqryScghpVt/OA0\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:28.714Z", + "time": 161, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 161 + } + }, + { + "_id": "e281e196584e342749f9137ef5f52b5e", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99017496996b5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:29 GMT" + }, + { + "name": "etag", + "value": "W/\"40-SPEIaxqwNgrPerxsVehA4Mc2YSw\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:28.877Z", + "time": 262, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 262 + } + }, + { + "_id": "a2aa4d213b82b2d932f85021e08ff0a5", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174983870ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:29 GMT" + }, + { + "name": "etag", + "value": "W/\"40-ypz1BtR9vGrNgplMnqFHflskWSQ\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:29.140Z", + "time": 341, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 341 + } + }, + { + "_id": "d09a9532fa24a0ddb1f6fcf975c1440a", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901749a5cd15913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:29 GMT" + }, + { + "name": "etag", + "value": "W/\"40-eTTX5ZC9b/wIluwj/KM0k+9Rkik\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:29.482Z", + "time": 255, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 255 + } + }, + { + "_id": "c2af0bd01bf47ea30352582ab9509c38", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901749bfdd4ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:30 GMT" + }, + { + "name": "etag", + "value": "W/\"40-nkeZzqF7RB7Sf8tLV2fEgsmcWeg\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:29.738Z", + "time": 308, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 308 + } + }, + { + "_id": "5b18d5a0c5bdf29b7efa4fa48fb08929", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901749dd8135913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:30 GMT" + }, + { + "name": "etag", + "value": "W/\"40-Ik+FFo//vczDSIdDQENIJWjKCUs\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:30.047Z", + "time": 355, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 355 + } + }, + { + "_id": "40e23df8de4e228681e77d206050f037", + "_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.3.0-canary.0" + } + ], + "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/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a01d4aad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:30 GMT" + }, + { + "name": "etag", + "value": "W/\"40-W7N8ixsRtC8IRQU5RNbQ7dUAPrY\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:30.403Z", + "time": 254, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 254 + } + }, + { + "_id": "6050a57b5c2fd774de74c96650746680", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 235, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [ + { + "name": "limit", + "value": "5" + } + ], + "url": "https://api.resend.com/audiences/59e81eca-46ce-44d8-a8c7-6eb2c2350f30/contacts?limit=5" + }, + "response": { + "bodySize": 921, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 921, + "text": "{\"object\":\"list\",\"has_more\":true,\"data\":[{\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:07.461063+00\",\"unsubscribed\":false},{\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.973541+00\",\"unsubscribed\":false},{\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.271228+00\",\"unsubscribed\":false},{\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:04.32337+00\",\"unsubscribed\":false},{\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:03.608808+00\",\"unsubscribed\":false}]}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a1bc125913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:30 GMT" + }, + { + "name": "etag", + "value": "W/\"399-ZD+CAlt2/t3k/gEhTBP59XSLanY\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 371, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:30.659Z", + "time": 154, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 154 + } + }, + { + "_id": "3b1c2334541543ace3170de6a4d80b80", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/59e81eca-46ce-44d8-a8c7-6eb2c2350f30" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"59e81eca-46ce-44d8-a8c7-6eb2c2350f30\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174a2aec3ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:31 GMT" + }, + { + "name": "etag", + "value": "W/\"50-M9ULFfz+EKzIEGEXBm8C8Sd92r4\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:30.815Z", + "time": 289, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 289 + } + } + ], + "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..f4d374d2 --- /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": "d9c62769c0014e5e376db57af8c7614e", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for listing\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 100, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 100, + "text": "{\"object\":\"audience\",\"id\":\"92112b37-2b3c-4aeb-83fb-2f473fa4d740\",\"name\":\"Test audience for listing\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174878f355913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "100" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:26 GMT" + }, + { + "name": "etag", + "value": "W/\"64-USObuVXxaszt+hyix9qHsXQUB0A\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:26.474Z", + "time": 159, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 159 + } + }, + { + "_id": "018799e15322462f5bc40d79b52822be", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174888b07ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:26 GMT" + }, + { + "name": "etag", + "value": "W/\"40-SPEIaxqwNgrPerxsVehA4Mc2YSw\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:26.634Z", + "time": 268, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 268 + } + }, + { + "_id": "160f1ba1f4266bf163c26002e15f195e", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901748a3fad5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:27 GMT" + }, + { + "name": "etag", + "value": "W/\"40-ypz1BtR9vGrNgplMnqFHflskWSQ\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:26.903Z", + "time": 327, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 327 + } + }, + { + "_id": "4614c250ed122a7ee4958dea76ad97c6", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901748c4f60ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:27 GMT" + }, + { + "name": "etag", + "value": "W/\"40-eTTX5ZC9b/wIluwj/KM0k+9Rkik\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:27.231Z", + "time": 281, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 281 + } + }, + { + "_id": "19c54bab554140a0c55d2e2b71fafef5", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901748e0da85913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:27 GMT" + }, + { + "name": "etag", + "value": "W/\"40-nkeZzqF7RB7Sf8tLV2fEgsmcWeg\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:27.512Z", + "time": 248, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 248 + } + }, + { + "_id": "9c330a59f1800cd2cb5d8a13f3bb4439", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9901748f9ae2ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:27 GMT" + }, + { + "name": "etag", + "value": "W/\"40-Ik+FFo//vczDSIdDQENIJWjKCUs\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:27.761Z", + "time": 238, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 238 + } + }, + { + "_id": "8c845d58009faa21e611093dd041f9f6", + "_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.3.0-canary.0" + } + ], + "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/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174911fc95913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:28 GMT" + }, + { + "name": "etag", + "value": "W/\"40-W7N8ixsRtC8IRQU5RNbQ7dUAPrY\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:28.001Z", + "time": 255, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 255 + } + }, + { + "_id": "2fbe917448561c27fcf7f4886a6c9b6a", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 227, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/92112b37-2b3c-4aeb-83fb-2f473fa4d740/contacts" + }, + "response": { + "bodySize": 1098, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 1098, + "text": "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"ef6b0bba-5e87-4360-8f12-493ee2d31adb\",\"email\":\"test.5@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:07.461063+00\",\"unsubscribed\":false},{\"id\":\"38ddf490-a2de-4ddd-ac89-361dd5fc2e35\",\"email\":\"test.4@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.973541+00\",\"unsubscribed\":false},{\"id\":\"8a57452b-164e-4e35-b7cf-41087d9dce9b\",\"email\":\"test.3@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:05.271228+00\",\"unsubscribed\":false},{\"id\":\"b2636f63-45f7-4fae-9636-3b9ed474099d\",\"email\":\"test.2@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:04.32337+00\",\"unsubscribed\":false},{\"id\":\"d2301d07-ed3a-4a67-bd3c-e68a2f402906\",\"email\":\"test.1@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:03.608808+00\",\"unsubscribed\":false},{\"id\":\"2ce5866a-84d9-4933-8d28-7ef402bb17d1\",\"email\":\"test.0@example.com\",\"first_name\":null,\"last_name\":null,\"created_at\":\"2025-10-17 16:47:01.981048+00\",\"unsubscribed\":false}]}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99017492bdbdad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:28 GMT" + }, + { + "name": "etag", + "value": "W/\"44a-VNxzWns+IrIaVg1bxBGHZwFOPsI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 371, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:28.258Z", + "time": 201, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 201 + } + }, + { + "_id": "5d335154a64b7a44a8c72229eb45da1e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/92112b37-2b3c-4aeb-83fb-2f473fa4d740" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"92112b37-2b3c-4aeb-83fb-2f473fa4d740\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99017493fa45ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:28 GMT" + }, + { + "name": "etag", + "value": "W/\"50-/gYbLxfuFX9Iy0Byw/iyMwl/ySI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:28.460Z", + "time": 248, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 248 + } + } + ], + "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..637a16c7 --- /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": "31be3e42567e9e6de269f27d108c276f", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 112, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 112, + "text": "{\"object\":\"audience\",\"id\":\"929b3690-e56f-4f69-9f7d-0d4005b8fd50\",\"name\":\"Test audience for non-existent delete\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174d0fb0dad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "112" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:38 GMT" + }, + { + "name": "etag", + "value": "W/\"70-MyhAdzWJr/jt7uIVtOIRMlY2yXA\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:38.219Z", + "time": 157, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 157 + } + }, + { + "_id": "b64b4983d1955e42ed83e2d0adb89468", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 267, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/929b3690-e56f-4f69-9f7d-0d4005b8fd50/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": "990174d1eb1f5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:38 GMT" + }, + { + "name": "etag", + "value": "W/\"54-rfEgMCeqSJYc1agGuHEGmuCuACI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:38.377Z", + "time": 163, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 163 + } + }, + { + "_id": "19e03a717661326db0cbaf1b8b9b4762", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/929b3690-e56f-4f69-9f7d-0d4005b8fd50" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"929b3690-e56f-4f69-9f7d-0d4005b8fd50\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174d2fa22ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:38 GMT" + }, + { + "name": "etag", + "value": "W/\"50-zhQy7WO8qBt7uw8FHWJFx8jonmE\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:38.540Z", + "time": 212, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 212 + } + } + ], + "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..fd02682b --- /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": "8e78d48529cfeb223b879f40f616663e", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 108, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 108, + "text": "{\"object\":\"audience\",\"id\":\"3810aa6d-856d-42e7-b1cb-05a3b21b8db6\",\"name\":\"Test audience for remove by email\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174c95895ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "108" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:37 GMT" + }, + { + "name": "etag", + "value": "W/\"6c-74EOpd7v3g6M6bUJrQnDTopoZpk\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:37.004Z", + "time": 146, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 146 + } + }, + { + "_id": "08b51636fb8cf0d312f13158cfbf9d99", + "_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.3.0-canary.0" + } + ], + "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/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"ff589dde-1acc-497a-b71d-736661a981fd\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174ca482f5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:37 GMT" + }, + { + "name": "etag", + "value": "W/\"40-umpVh6I6kDpYeb5WAHwZdvNxA6g\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:37.150Z", + "time": 337, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 337 + } + }, + { + "_id": "e48b0940904311dbe91c643b2e98bd50", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 263, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/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": "990174cc6b21ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:37 GMT" + }, + { + "name": "etag", + "value": "W/\"50-Xsi7eJcodAoVMeQ8sbTFSvP7E3I\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:37.487Z", + "time": 333, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 333 + } + }, + { + "_id": "d854344ff2e35283becd868efc86a911", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 260, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/3810aa6d-856d-42e7-b1cb-05a3b21b8db6/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": "990174ce7a08ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:37 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-17T17:18:37.821Z", + "time": 153, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 153 + } + }, + { + "_id": "6c733407504f42f7f3939e22c93234e9", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/3810aa6d-856d-42e7-b1cb-05a3b21b8db6" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"3810aa6d-856d-42e7-b1cb-05a3b21b8db6\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174cf6d67ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:38 GMT" + }, + { + "name": "etag", + "value": "W/\"50-Zl3VlcxhpmxSBHvwOqi6U3gLgmw\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:37.975Z", + "time": 242, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 242 + } + } + ], + "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..03b17d52 --- /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": "e804c3a44a1cdbc7eacdf402a7b4f39e", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "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/segments" + }, + "response": { + "bodySize": 105, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 105, + "text": "{\"object\":\"audience\",\"id\":\"70771159-5b4e-476e-9783-35bd76c5aea8\",\"name\":\"Test audience for remove by ID\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174c0caf1ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "105" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:35 GMT" + }, + { + "name": "etag", + "value": "W/\"69-N7156RrBa/10i7mNjcWTamF3nmU\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 341, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:35.571Z", + "time": 240, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 240 + } + }, + { + "_id": "8fa48ffd03607392f9088eb5ad03e4ae", + "_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.3.0-canary.0" + } + ], + "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/70771159-5b4e-476e-9783-35bd76c5aea8/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174c1ebe75913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:36 GMT" + }, + { + "name": "etag", + "value": "W/\"40-jfysRk4N8NfGNJcXomgbzIJCM+g\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:35.812Z", + "time": 343, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 343 + } + }, + { + "_id": "175c65be846ad8604e0b79377b5ffda0", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 267, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/audiences/70771159-5b4e-476e-9783-35bd76c5aea8/contacts/a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"object\":\"contact\",\"contact\":\"a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174c40e89ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:36 GMT" + }, + { + "name": "etag", + "value": "W/\"54-xZ3FSZahyWRBkRDBEOXvp73ajdM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:36.157Z", + "time": 352, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 352 + } + }, + { + "_id": "edd0d9a66149d94815c7ce04827ba80b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/70771159-5b4e-476e-9783-35bd76c5aea8/contacts/a93069e5-3aa9-44de-9e18-ec9e2ebf1d8d" + }, + "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": "990174c6e826ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:36 GMT" + }, + { + "name": "etag", + "value": "W/\"43-Hlm9sCxABe1C/S9NDIZg0PaqfAM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-17T17:18:36.510Z", + "time": 252, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 252 + } + }, + { + "_id": "a07617b83e6a4dc407533e0144d9e12b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/70771159-5b4e-476e-9783-35bd76c5aea8" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"70771159-5b4e-476e-9783-35bd76c5aea8\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174c7db80ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:36 GMT" + }, + { + "name": "etag", + "value": "W/\"50-7jG26XSURJ4Z0dn6tkuFlzd+srA\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:36.764Z", + "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-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..30ff023e --- /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": "7e436bec9db93ade6fd32ba82b86a603", + "_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.3.0-canary.0" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test audience for update\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 99, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 99, + "text": "{\"object\":\"audience\",\"id\":\"ab119ae4-44bf-4f3f-b809-235002703ebd\",\"name\":\"Test audience for update\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174b6c817ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "99" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:34 GMT" + }, + { + "name": "etag", + "value": "W/\"63-VCultuXnDFeQp3w+Em+XiEg+DPk\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:34.036Z", + "time": 284, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 284 + } + }, + { + "_id": "7fec48e2c530889d8db6605add34ce50", + "_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.3.0-canary.0" + } + ], + "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/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174b93ffd5913-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "64" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:34 GMT" + }, + { + "name": "etag", + "value": "W/\"40-yAc35ksyId+b7UcvfJAExdFUHYE\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-17T17:18:34.321Z", + "time": 450, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 450 + } + }, + { + "_id": "2c9fa319407a8ec99d3a13f3a7b62e20", + "_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.3.0-canary.0" + } + ], + "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/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts/45bf1c68-22ec-4acd-90d3-06c4c4784cd4" + }, + "response": { + "bodySize": 64, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 64, + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174bb6f7dad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:35 GMT" + }, + { + "name": "etag", + "value": "W/\"40-yAc35ksyId+b7UcvfJAExdFUHYE\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:34.773Z", + "time": 265, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 265 + } + }, + { + "_id": "348f89902dbcc5e2615eda52449d93de", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 264, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/audiences/ab119ae4-44bf-4f3f-b809-235002703ebd/contacts/45bf1c68-22ec-4acd-90d3-06c4c4784cd4" + }, + "response": { + "bodySize": 205, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 205, + "text": "{\"object\":\"contact\",\"id\":\"45bf1c68-22ec-4acd-90d3-06c4c4784cd4\",\"email\":\"test-update@example.com\",\"first_name\":\"Updated\",\"last_name\":\"Name\",\"created_at\":\"2025-10-17 16:47:19.18697+00\",\"unsubscribed\":false}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174bd1da9ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:35 GMT" + }, + { + "name": "etag", + "value": "W/\"cd-rNTm2DvbIH83YX1VRDiNRc+nyHo\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:35.040Z", + "time": 171, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 171 + } + }, + { + "_id": "9a669484e44a6f4df4ee6ab609ac15ce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.0" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/ab119ae4-44bf-4f3f-b809-235002703ebd" + }, + "response": { + "bodySize": 80, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 80, + "text": "{\"object\":\"audience\",\"id\":\"ab119ae4-44bf-4f3f-b809-235002703ebd\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "990174be2958ad76-PDX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 17 Oct 2025 17:18:35 GMT" + }, + { + "name": "etag", + "value": "W/\"50-C7lWpGGjIiJvyGOaVuojCUZ58hY\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-17T17:18:35.212Z", + "time": 352, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 352 + } + } + ], + "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..4bd9d9dc --- /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('missing_required_field'); + }); + }); + + 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..bef17c0d 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 () => { @@ -138,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', () => { @@ -351,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', () => { @@ -454,5 +701,34 @@ describe('Contacts', () => { } `); }); + + it('removes a contact by string id', async () => { + const response: RemoveContactsResponseSuccess = { + contact: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + object: 'contact', + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.remove('3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223'), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "contact": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223", + "deleted": true, + "object": "contact", + }, + "error": null, +} +`); + }); }); }); diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts index fd469ca9..730be8fa 100644 --- a/src/contacts/contacts.ts +++ b/src/contacts/contacts.ts @@ -12,6 +12,7 @@ import type { GetContactResponseSuccess, } from './interfaces/get-contact.interface'; import type { + ListAudienceContactsOptions, ListContactsOptions, ListContactsResponse, ListContactsResponseSuccess, @@ -26,14 +27,36 @@ import type { UpdateContactResponse, UpdateContactResponseSuccess, } from './interfaces/update-contact.interface'; +import { ContactSegments } from './segments/contact-segments'; +import { ContactTopics } from './topics/contact-topics'; export class Contacts { - constructor(private readonly resend: Resend) {} + readonly topics: ContactTopics; + readonly segments: ContactSegments; + + constructor(private readonly resend: Resend) { + this.topics = new ContactTopics(this.resend); + this.segments = new ContactSegments(this.resend); + } async create( payload: CreateContactOptions, options: CreateContactRequestOptions = {}, ): Promise { + if (!payload.audienceId) { + const data = await this.resend.post( + '/contacts', + { + unsubscribed: payload.unsubscribed, + email: payload.email, + first_name: payload.firstName, + last_name: payload.lastName, + }, + options, + ); + return data; + } + const data = await this.resend.post( `/audiences/${payload.audienceId}/contacts`, { @@ -47,18 +70,33 @@ export class Contacts { return data; } - async list(options: ListContactsOptions): Promise { + async list( + options: ListContactsOptions | ListAudienceContactsOptions = {}, + ): Promise { + if (!('audienceId' in options) || options.audienceId === undefined) { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/contacts?${queryString}` : '/contacts'; + const data = await this.resend.get(url); + return data; + } + const { audienceId, ...paginationOptions } = options; const queryString = buildPaginationQuery(paginationOptions); const url = queryString ? `/audiences/${audienceId}/contacts?${queryString}` : `/audiences/${audienceId}/contacts`; - const data = await this.resend.get(url); return data; } async get(options: GetContactOptions): Promise { + if (typeof options === 'string') { + const data = await this.resend.get( + `/contacts/${options}`, + ); + return data; + } + if (!options.id && !options.email) { return { data: null, @@ -70,6 +108,13 @@ export class Contacts { }; } + if (!options.audienceId) { + const data = await this.resend.get( + `/contacts/${options?.email ? options?.email : options?.id}`, + ); + return data; + } + const data = await this.resend.get( `/audiences/${options.audienceId}/contacts/${options?.email ? options?.email : options?.id}`, ); @@ -88,6 +133,18 @@ export class Contacts { }; } + if (!options.audienceId) { + const data = await this.resend.patch( + `/contacts/${options?.email ? options?.email : options?.id}`, + { + unsubscribed: options.unsubscribed, + first_name: options.firstName, + last_name: options.lastName, + }, + ); + return data; + } + const data = await this.resend.patch( `/audiences/${options.audienceId}/contacts/${options?.email ? options?.email : options?.id}`, { @@ -100,6 +157,13 @@ export class Contacts { } async remove(payload: RemoveContactOptions): Promise { + if (typeof payload === 'string') { + const data = await this.resend.delete( + `/contacts/${payload}`, + ); + return data; + } + if (!payload.id && !payload.email) { return { data: null, @@ -111,11 +175,19 @@ export class Contacts { }; } + if (!payload.audienceId) { + const data = await this.resend.delete( + `/contacts/${payload?.email ? payload?.email : payload?.id}`, + ); + return data; + } + const data = await this.resend.delete( `/audiences/${payload.audienceId}/contacts/${ payload?.email ? payload?.email : payload?.id }`, ); + return data; } } diff --git a/src/contacts/interfaces/create-contact-options.interface.ts b/src/contacts/interfaces/create-contact-options.interface.ts index ff73f25b..85402aae 100644 --- a/src/contacts/interfaces/create-contact-options.interface.ts +++ b/src/contacts/interfaces/create-contact-options.interface.ts @@ -3,7 +3,7 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; export interface CreateContactOptions { - audienceId: string; + audienceId?: string; email: string; unsubscribed?: boolean; firstName?: string; diff --git a/src/contacts/interfaces/get-contact.interface.ts b/src/contacts/interfaces/get-contact.interface.ts index 69b9f978..1e70aff6 100644 --- a/src/contacts/interfaces/get-contact.interface.ts +++ b/src/contacts/interfaces/get-contact.interface.ts @@ -1,9 +1,11 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; -export type GetContactOptions = { - audienceId: string; -} & SelectingField; +export type GetContactOptions = + | string + | ({ + audienceId?: string; + } & SelectingField); export interface GetContactResponseSuccess extends Pick< diff --git a/src/contacts/interfaces/list-contacts.interface.ts b/src/contacts/interfaces/list-contacts.interface.ts index 43fa3118..72bb9bf9 100644 --- a/src/contacts/interfaces/list-contacts.interface.ts +++ b/src/contacts/interfaces/list-contacts.interface.ts @@ -1,11 +1,16 @@ +// list-contacts.interface.ts import type { PaginationOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; -export type ListContactsOptions = { +export type ListAudienceContactsOptions = { audienceId: string; } & PaginationOptions; +export type ListContactsOptions = PaginationOptions & { + audienceId?: string; +}; + export interface ListContactsResponseSuccess { object: 'list'; data: Contact[]; diff --git a/src/contacts/interfaces/remove-contact.interface.ts b/src/contacts/interfaces/remove-contact.interface.ts index 59d2e2c0..c3e4f227 100644 --- a/src/contacts/interfaces/remove-contact.interface.ts +++ b/src/contacts/interfaces/remove-contact.interface.ts @@ -7,9 +7,11 @@ export type RemoveContactsResponseSuccess = { contact: string; }; -export type RemoveContactOptions = SelectingField & { - audienceId: string; -}; +export type RemoveContactOptions = + | string + | (SelectingField & { + audienceId?: string; + }); export type RemoveContactsResponse = | { diff --git a/src/contacts/interfaces/update-contact.interface.ts b/src/contacts/interfaces/update-contact.interface.ts index c5a60ff5..49831c7d 100644 --- a/src/contacts/interfaces/update-contact.interface.ts +++ b/src/contacts/interfaces/update-contact.interface.ts @@ -2,7 +2,7 @@ import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; export type UpdateContactOptions = { - audienceId: string; + audienceId?: string; unsubscribed?: boolean; firstName?: string; lastName?: string; diff --git a/src/contacts/segments/contact-segments.spec.ts b/src/contacts/segments/contact-segments.spec.ts new file mode 100644 index 00000000..cdd9825a --- /dev/null +++ b/src/contacts/segments/contact-segments.spec.ts @@ -0,0 +1,294 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + AddContactSegmentOptions, + AddContactSegmentResponseSuccess, +} from './interfaces/add-contact-segment.interface'; +import type { + ListContactSegmentsOptions, + ListContactSegmentsResponseSuccess, +} from './interfaces/list-contact-segments.interface'; +import type { + RemoveContactSegmentOptions, + RemoveContactSegmentResponseSuccess, +} from './interfaces/remove-contact-segment.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('ContactSegments', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + describe('list', () => { + it('gets contact segments by email', async () => { + const options: ListContactSegmentsOptions = { + email: 'carolina@resend.com', + }; + const response: ListContactSegmentsResponseSuccess = { + object: 'list', + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Segment', + created_at: '2021-01-01T00:00:00.000Z', + }, + { + id: 'd7e1e488-ae2c-4255-a40c-a4db3af7ed0c', + name: 'Another Segment', + 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.segments.list(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2021-01-01T00:00:00.000Z", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Segment", + }, + { + "created_at": "2021-01-02T00:00:00.000Z", + "id": "d7e1e488-ae2c-4255-a40c-a4db3af7ed0c", + "name": "Another Segment", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, +} +`); + }); + + it('gets contact segments by ID', async () => { + const options: ListContactSegmentsOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + limit: 1, + after: '584a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }; + const response: ListContactSegmentsResponseSuccess = { + object: 'list', + data: [ + { + id: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + name: 'Test Segment', + 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.segments.list(options), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2021-01-01T00:00:00.000Z", + "id": "c7e1e488-ae2c-4255-a40c-a4db3af7ed0b", + "name": "Test Segment", + }, + ], + "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.segments.list( + options as ListContactSegmentsOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + }); + + describe('add', () => { + it('adds a contact to an audience', async () => { + const options: AddContactSegmentOptions = { + email: 'carolina@resend.com', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: AddContactSegmentResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.segments.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: AddContactSegmentOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: AddContactSegmentResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + }; + + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.segments.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.segments.add( + options as AddContactSegmentOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + }); + + describe('remove', () => { + it('removes a contact from an audience', async () => { + const options: RemoveContactSegmentOptions = { + email: 'carolina@resend.com', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: RemoveContactSegmentResponseSuccess = { + 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.segments.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: RemoveContactSegmentOptions = { + contactId: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + segmentId: 'c7e1e488-ae2c-4255-a40c-a4db3af7ed0b', + }; + + const response: RemoveContactSegmentResponseSuccess = { + 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.segments.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.segments.remove( + options as RemoveContactSegmentOptions, + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`id\` or \`email\` field.", + "name": "missing_required_field", + "statusCode": null, + }, +} +`); + }); + }); +}); diff --git a/src/contacts/segments/contact-segments.ts b/src/contacts/segments/contact-segments.ts new file mode 100644 index 00000000..641d8985 --- /dev/null +++ b/src/contacts/segments/contact-segments.ts @@ -0,0 +1,85 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + AddContactSegmentOptions, + AddContactSegmentResponse, + AddContactSegmentResponseSuccess, +} from './interfaces/add-contact-segment.interface'; +import type { + ListContactSegmentsOptions, + ListContactSegmentsResponse, + ListContactSegmentsResponseSuccess, +} from './interfaces/list-contact-segments.interface'; +import type { + RemoveContactSegmentOptions, + RemoveContactSegmentResponse, + RemoveContactSegmentResponseSuccess, +} from './interfaces/remove-contact-segment.interface'; + +export class ContactSegments { + constructor(private readonly resend: Resend) {} + + async list( + options: ListContactSegmentsOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/contacts/${identifier}/segments?${queryString}` + : `/contacts/${identifier}/segments`; + + const data = await this.resend.get(url); + return data; + } + + async add( + options: AddContactSegmentOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + return this.resend.post( + `/contacts/${identifier}/segments/${options.segmentId}`, + ); + } + + async remove( + options: RemoveContactSegmentOptions, + ): Promise { + if (!options.contactId && !options.email) { + return { + data: null, + error: { + message: 'Missing `id` or `email` field.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + + const identifier = options.email ? options.email : options.contactId; + return this.resend.delete( + `/contacts/${identifier}/segments/${options.segmentId}`, + ); + } +} diff --git a/src/contacts/segments/interfaces/add-contact-segment.interface.ts b/src/contacts/segments/interfaces/add-contact-segment.interface.ts new file mode 100644 index 00000000..ac892f19 --- /dev/null +++ b/src/contacts/segments/interfaces/add-contact-segment.interface.ts @@ -0,0 +1,20 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type AddContactSegmentOptions = ContactSegmentsBaseOptions & { + segmentId: string; +}; + +export interface AddContactSegmentResponseSuccess { + id: string; +} + +export type AddContactSegmentResponse = + | { + data: AddContactSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/segments/interfaces/contact-segments.interface.ts b/src/contacts/segments/interfaces/contact-segments.interface.ts new file mode 100644 index 00000000..cb808201 --- /dev/null +++ b/src/contacts/segments/interfaces/contact-segments.interface.ts @@ -0,0 +1,9 @@ +export type ContactSegmentsBaseOptions = + | { + contactId: string; + email?: never; + } + | { + contactId?: never; + email: string; + }; diff --git a/src/contacts/segments/interfaces/list-contact-segments.interface.ts b/src/contacts/segments/interfaces/list-contact-segments.interface.ts new file mode 100644 index 00000000..2e2b82da --- /dev/null +++ b/src/contacts/segments/interfaces/list-contact-segments.interface.ts @@ -0,0 +1,22 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../../common/interfaces/pagination-options.interface'; +import type { ErrorResponse } from '../../../interfaces'; +import type { Segment } from '../../../segments/interfaces/segment'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type ListContactSegmentsOptions = PaginationOptions & + ContactSegmentsBaseOptions; + +export type ListContactSegmentsResponseSuccess = PaginatedData; + +export type ListContactSegmentsResponse = + | { + data: ListContactSegmentsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/segments/interfaces/remove-contact-segment.interface.ts b/src/contacts/segments/interfaces/remove-contact-segment.interface.ts new file mode 100644 index 00000000..11951347 --- /dev/null +++ b/src/contacts/segments/interfaces/remove-contact-segment.interface.ts @@ -0,0 +1,21 @@ +import type { ErrorResponse } from '../../../interfaces'; +import type { ContactSegmentsBaseOptions } from './contact-segments.interface'; + +export type RemoveContactSegmentOptions = ContactSegmentsBaseOptions & { + segmentId: string; +}; + +export interface RemoveContactSegmentResponseSuccess { + id: string; + deleted: boolean; +} + +export type RemoveContactSegmentResponse = + | { + data: RemoveContactSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/topics/contact-topics.spec.ts b/src/contacts/topics/contact-topics.spec.ts new file mode 100644 index 00000000..20a9f875 --- /dev/null +++ b/src/contacts/topics/contact-topics.spec.ts @@ -0,0 +1,308 @@ +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", + "statusCode": null, + }, +} +`); + }); + + 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", + "statusCode": null, + }, +} +`); + }); + }); +}); diff --git a/src/contacts/topics/contact-topics.ts b/src/contacts/topics/contact-topics.ts new file mode 100644 index 00000000..e87bd4a4 --- /dev/null +++ b/src/contacts/topics/contact-topics.ts @@ -0,0 +1,63 @@ +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.', + statusCode: null, + 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.', + statusCode: null, + 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 ebfb3943..de9d7577 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,14 +13,19 @@ 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 () => { 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: [ @@ -64,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', }; @@ -82,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", @@ -127,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", @@ -174,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: [ @@ -233,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", @@ -325,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: [ @@ -376,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", @@ -427,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', @@ -434,6 +464,7 @@ describe('Domains', () => { status: 'not_started', created_at: '2023-04-07T23:13:20.417116+00:00', region: 'us-east-1', + capability: 'receive', }, ], }; @@ -571,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', @@ -601,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, + }, ], }; @@ -617,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", @@ -647,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 eda221df..d4df7587 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 () => { @@ -61,6 +66,7 @@ describe('Emails', () => { to: 'user@resend.com', subject: 'Not Idempotent Test', html: '

Test

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

Test

","subject":"Not Idempotent Test","to":"user@resend.com","topic_id":"9f31e56e-3083-46cf-8e96-c6995e0e576a"}', + ); + + //@ts-expect-error + const hasIdempotencyKey = lastCall[1]?.headers.has('Idempotency-Key'); + expect(hasIdempotencyKey).toBeFalsy(); + + //@ts-expect-error + const usedIdempotencyKey = lastCall[1]?.headers.get('Idempotency-Key'); + expect(usedIdempotencyKey).toBeNull(); }); it('sends the Idempotency-Key header when idempotencyKey is provided', async () => { @@ -391,6 +407,188 @@ 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', + statusCode: 404, + }; + + 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", + "statusCode": 404, + }, +} +`); + }); + }); }); describe('get', () => { 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/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/emails/receiving/interfaces/get-inbound-email.interface.ts b/src/emails/receiving/interfaces/get-inbound-email.interface.ts new file mode 100644 index 00000000..4e19c00d --- /dev/null +++ b/src/emails/receiving/interfaces/get-inbound-email.interface.ts @@ -0,0 +1,33 @@ +import type { ErrorResponse } from '../../../interfaces'; + +export interface GetInboundEmailResponseSuccess { + 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; + headers: Record; + attachments: Array<{ + id: string; + filename: string; + content_type: string; + content_id: string; + content_disposition: string; + }>; +} + +export type GetInboundEmailResponse = + | { + data: GetInboundEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/emails/receiving/interfaces/index.ts b/src/emails/receiving/interfaces/index.ts new file mode 100644 index 00000000..3295255d --- /dev/null +++ b/src/emails/receiving/interfaces/index.ts @@ -0,0 +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 new file mode 100644 index 00000000..d474e6bc --- /dev/null +++ b/src/emails/receiving/receiving.spec.ts @@ -0,0 +1,395 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../../interfaces'; +import { Resend } from '../../resend'; +import type { + GetInboundEmailResponseSuccess, + ListInboundEmailsResponseSuccess, +} from './interfaces'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Receiving', () => { + afterEach(() => fetchMock.resetMocks()); + + describe('get', () => { + describe('when inbound email not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Email not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = resend.emails.receiving.get( + '61cda979-919d-4b9d-9638-c148b93ff410', + ); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Email not found", + "name": "not_found", + }, +} +`); + }); + }); + + describe('when inbound email found', () => { + it('returns inbound email', async () => { + const apiResponse: GetInboundEmailResponseSuccess = { + 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'], + headers: { + example: 'value', + }, + 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.emails.receiving.get( + '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + ); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "attachments": [ + { + "content_disposition": "attachment", + "content_id": "cid_123", + "content_type": "application/pdf", + "filename": "document.pdf", + "id": "att_123", + }, + ], + "bcc": null, + "cc": [ + "cc@example.com", + ], + "created_at": "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", + "reply_to": [ + "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: GetInboundEmailResponseSuccess = { + 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, + headers: {}, + attachments: [], + }; + + fetchMock.mockOnce(JSON.stringify(apiResponse), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', + }, + }); + + const result = await resend.emails.receiving.get( + '67d9bcdb-5a02-42d7-8da9-0d6feea18cff', + ); + + expect(result).toMatchInlineSnapshot(` +{ + "data": { + "attachments": [], + "bcc": null, + "cc": null, + "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", + "reply_to": null, + "subject": "Test inbound email", + "text": "hello world", + "to": [ + "received@example.com", + ], + }, + "error": null, +} +`); + }); + }); + }); + + 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: ListInboundEmailsResponseSuccess = { + 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, + }, + ], + }, + { + 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", + }, + ], + "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 new file mode 100644 index 00000000..8592827b --- /dev/null +++ b/src/emails/receiving/receiving.ts @@ -0,0 +1,36 @@ +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) {} + + async get(id: string): Promise { + const data = await this.resend.get( + `/emails/receiving/${id}`, + ); + + 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; + } +} diff --git a/src/index.ts b/src/index.ts index e865109e..838e4a85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,12 @@ export * from './api-keys/interfaces'; -export * from './audiences/interfaces'; +export * from './attachments/receiving/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; export * from './common/interfaces'; export * from './contacts/interfaces'; export * from './domains/interfaces'; export * from './emails/interfaces'; +export * from './emails/receiving/interfaces'; export { ErrorResponse } from './interfaces'; export { Resend } from './resend'; +export * from './segments/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index 217535e6..23ba683d 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -1,6 +1,6 @@ import { version } from '../package.json'; import { ApiKeys } from './api-keys/api-keys'; -import { Audiences } from './audiences/audiences'; +import { Attachments } from './attachments/attachments'; import { Batch } from './batch/batch'; import { Broadcasts } from './broadcasts/broadcasts'; import type { GetOptions, PostOptions, PutOptions } from './common/interfaces'; @@ -10,6 +10,9 @@ import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; import type { ErrorResponse } from './interfaces'; +import { Segments } from './segments/segments'; +import { Templates } from './templates/templates'; +import { Topics } from './topics/topics'; import { Webhooks } from './webhooks/webhooks'; const defaultBaseUrl = 'https://api.resend.com'; @@ -27,13 +30,20 @@ export class Resend { private readonly headers: Headers; readonly apiKeys = new ApiKeys(this); - readonly audiences = new Audiences(this); + readonly attachments = new Attachments(this); + readonly segments = new Segments(this); + /** + * @deprecated Use segments instead + */ + readonly audiences = this.segments; readonly batch = new Batch(this); readonly broadcasts = new Broadcasts(this); readonly contacts = new Contacts(this); readonly domains = new Domains(this); readonly emails = new Emails(this); readonly webhooks = new Webhooks(); + readonly templates = new Templates(this); + readonly topics = new Topics(this); constructor(readonly key?: string) { if (!key) { diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-create-creates-an-audience_3138926239/recording.har new file mode 100644 index 00000000..a1645943 --- /dev/null +++ b/src/segments/__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": "58407851aec32b12e835bf9b5e7d41cc", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 23, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 86, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 86, + "text": "{\"object\":\"segment\",\"id\":\"3f1e4daf-204e-4b96-bf69-6a567da76e60\",\"name\":\"Test Segment\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295215f2e7c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "86" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"56-qfG19NkGbeZB0nV/O/jZy649j70\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:51.700Z", + "time": 323, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 323 + } + }, + { + "_id": "bd773a937405a4cd7f10a04d2eaab0a0", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/3f1e4daf-204e-4b96-bf69-6a567da76e60" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"3f1e4daf-204e-4b96-bf69-6a567da76e60\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229522db627c91-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-RV03htyYnxmUepbfNF0378P8Gxc\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:52.026Z", + "time": 302, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 302 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-create-handles-validation-errors_1232029368/recording.har new file mode 100644 index 00000000..a48882d7 --- /dev/null +++ b/src/segments/__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": "69547be0d4508acfcb730bf8e485468b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"statusCode\":422,\"message\":\"Missing `name` field.\",\"name\":\"missing_required_field\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229524798a7c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "84" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"54-b7tWVBvPczzJWDVqTkO4kHnV3MM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-21T17:47:52.334Z", + "time": 128, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 128 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-get-retrieves-an-audience-by-id_604392511/recording.har new file mode 100644 index 00000000..b182855b --- /dev/null +++ b/src/segments/__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": "d2182e2e249ebf70f05e83b4d9b046e2", + "_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.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Audience for Get\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 95, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 95, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"name\":\"Test Audience for Get\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295254d517c91-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "95" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"5f-eRrNsiyD7BwoBQRhMxHsI6J5rZw\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:52.467Z", + "time": 139, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 139 + } + }, + { + "_id": "09649798d911024796f0e156eae8630e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/608d4e2d-f5f3-42a6-8bfb-9da90b6316e9" + }, + "response": { + "bodySize": 140, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 140, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"name\":\"Test Audience for Get\",\"created_at\":\"2025-10-21 17:47:52.582248+00\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295262ae67c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"8c-1FQ3+QJKCtjF6djxHI696h01s/c\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:52.606Z", + "time": 134, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 134 + } + }, + { + "_id": "1679cbd7816fde98ed4677fbb020577b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/608d4e2d-f5f3-42a6-8bfb-9da90b6316e9" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"608d4e2d-f5f3-42a6-8bfb-9da90b6316e9\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295270ba97c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:52 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-AV6E1rZlJDL/ZxqT+ZoqlaES6QI\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "14" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:52.742Z", + "time": 218, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 218 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-get-returns-error-for-non-existent-audience_2005927905/recording.har new file mode 100644 index 00000000..438e9e74 --- /dev/null +++ b/src/segments/__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": "ca62098e714a9be90b780b671f4db454", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295285ce97c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:53 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:47:52.966Z", + "time": 131, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 131 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-remove-appears-to-remove-an-audience-that-never-existed_2756217780/recording.har new file mode 100644 index 00000000..da77ae3e --- /dev/null +++ b/src/segments/__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": "96234cabd1d6945089d83f39f6464b8c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922952c5ff07c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:53 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-BAdct1U+nJquUIJWAOwQbWvPxJk\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:53.594Z", + "time": 218, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 218 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har b/src/segments/__recordings__/Audiences-Integration-Tests-remove-removes-an-audience_2396271215/recording.har new file mode 100644 index 00000000..b68d0ea8 --- /dev/null +++ b/src/segments/__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": "a3b883e60dd1515ce255ac720d46225f", + "_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.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Audience to Remove\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 97, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 97, + "text": "{\"object\":\"segment\",\"id\":\"fe8ae627-a12c-4c20-bb0f-47b383e51b69\",\"name\":\"Test Audience to Remove\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295293d967c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "97" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:53 GMT" + }, + { + "name": "etag", + "value": "W/\"61-lRIRLIjoHKaMYX9xrsltZaJHeKo\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:53.100Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + }, + { + "_id": "c484b9f9a5c26dc30ba0c965ebe53486", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/fe8ae627-a12c-4c20-bb0f-47b383e51b69" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"fe8ae627-a12c-4c20-bb0f-47b383e51b69\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922952a092c7c91-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:53 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-pQD64omrG7Z4Y1TKUre8nduwq3U\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:53.234Z", + "time": 218, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 218 + } + }, + { + "_id": "2e4f579f22a887ce6af131c76729eb07", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/fe8ae627-a12c-4c20-bb0f-47b383e51b69" + }, + "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": "9922952b6f517c7d-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:53 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:47:53.455Z", + "time": 134, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 134 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har new file mode 100644 index 00000000..18424c74 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-create-creates-a-segment_519994591/recording.har @@ -0,0 +1,229 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > create > creates a segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "58407851aec32b12e835bf9b5e7d41cc", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 23, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 86, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 86, + "text": "{\"object\":\"segment\",\"id\":\"e87493a1-d7ab-4299-a989-3a22ad5e5b26\",\"name\":\"Test Segment\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922954e09b60feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "86" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"56-qSYXtr2AREWbb+hM4m4hVm/up98\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:58.904Z", + "time": 233, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 233 + } + }, + { + "_id": "327f4f891881b8f6b7fc5ec03753ce0e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/e87493a1-d7ab-4299-a989-3a22ad5e5b26" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"e87493a1-d7ab-4299-a989-3a22ad5e5b26\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922954f4abbb75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-qLdr3x0KjAsapwMZNe4KA39v3pM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:47:59.140Z", + "time": 599, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 599 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har new file mode 100644 index 00000000..13b28ad9 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-create-handles-validation-errors_3457392545/recording.har @@ -0,0 +1,122 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > create > handles validation errors", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "69547be0d4508acfcb730bf8e485468b", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 84, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 84, + "text": "{\"statusCode\":422,\"message\":\"Missing `name` field.\",\"name\":\"missing_required_field\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229552cda00feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "84" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:47:59 GMT" + }, + { + "name": "etag", + "value": "W/\"54-b7tWVBvPczzJWDVqTkO4kHnV3MM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 422, + "statusText": "Unprocessable Entity" + }, + "startedDateTime": "2025-10-21T17:47:59.744Z", + "time": 134, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 134 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har new file mode 100644 index 00000000..c4b12158 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-get-retrieves-a-segment-by-id_2413577961/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > get > retrieves a segment by id", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "e35145f1426757232786d3d46a3d19ce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 31, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment for Get\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 94, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 94, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"name\":\"Test Segment for Get\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295539c9db75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "94" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"5e-M9p/d6gj5SqtQN/w8yk2dlkIRjA\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:47:59.883Z", + "time": 414, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 414 + } + }, + { + "_id": "e595a526a3df2c24c504758eecf76f4d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/1c429758-4d0a-40ca-9a23-c53d3151bc48" + }, + "response": { + "bodySize": 139, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 139, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"name\":\"Test Segment for Get\",\"created_at\":\"2025-10-21 17:48:00.256406+00\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955638990feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"8b-GoBRl3vyVwuAypGOyLm3EXUOIRU\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:00.299Z", + "time": 130, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 130 + } + }, + { + "_id": "36ded9e5fc6246f8668bffb6b40639ce", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/1c429758-4d0a-40ca-9a23-c53d3151bc48" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"1c429758-4d0a-40ca-9a23-c53d3151bc48\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955709730feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-mTt3CfkfwDGk7hS1dpd9eLT7Ya8\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "17" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:00.430Z", + "time": 331, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 331 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har new file mode 100644 index 00000000..7360be41 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-get-returns-error-for-non-existent-segment_3137910161/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > get > returns error for non-existent segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "ca62098e714a9be90b780b671f4db454", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "992295592b6d0feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:00 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "16" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:48:00.768Z", + "time": 128, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 128 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har new file mode 100644 index 00000000..20419078 --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-remove-appears-to-remove-a-segment-that-never-existed_2688528828/recording.har @@ -0,0 +1,121 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > remove > appears to remove a segment that never existed", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "96234cabd1d6945089d83f39f6464b8c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/00000000-0000-0000-0000-000000000000" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955d0eb90feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-BAdct1U+nJquUIJWAOwQbWvPxJk\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "18" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:01.390Z", + "time": 225, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 225 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har b/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har new file mode 100644 index 00000000..926786ff --- /dev/null +++ b/src/segments/__recordings__/Segments-Integration-Tests-remove-removes-a-segment_276723915/recording.har @@ -0,0 +1,336 @@ +{ + "log": { + "_recordingName": "Segments Integration Tests > remove > removes a segment", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "ffd095b200e0c2284c789f801615cb95", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 33, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 181, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"name\":\"Test Segment to Remove\"}" + }, + "queryString": [], + "url": "https://api.resend.com/segments" + }, + "response": { + "bodySize": 96, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 96, + "text": "{\"object\":\"segment\",\"id\":\"8739500c-d215-46fa-b7fe-c843413bb018\",\"name\":\"Test Segment to Remove\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "99229559fc250feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "96" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"60-exsu0Qq8e4LfrSvB1/Pqda43928\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "15" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + } + ], + "headersSize": 340, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 201, + "statusText": "Created" + }, + "startedDateTime": "2025-10-21T17:48:00.900Z", + "time": 149, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 149 + } + }, + { + "_id": "4a73d3600c3e436264288a0c729945ae", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 220, + "httpVersion": "HTTP/1.1", + "method": "DELETE", + "queryString": [], + "url": "https://api.resend.com/segments/8739500c-d215-46fa-b7fe-c843413bb018" + }, + "response": { + "bodySize": 79, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 79, + "text": "{\"object\":\"segment\",\"id\":\"8739500c-d215-46fa-b7fe-c843413bb018\",\"deleted\":true}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955aee00b75a-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"4f-oJkvyjPPjL1h+HpXoubDEW58TNo\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "14" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-10-21T17:48:01.050Z", + "time": 202, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 202 + } + }, + { + "_id": "aeb73a02813509d93917f83332bee7af", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "Bearer re_REDACTED_API_KEY" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "resend-node:6.3.0-canary.1" + } + ], + "headersSize": 217, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.resend.com/segments/8739500c-d215-46fa-b7fe-c843413bb018" + }, + "response": { + "bodySize": 68, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 68, + "text": "{\"statusCode\":404,\"message\":\"Audience not found\",\"name\":\"not_found\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "cf-ray", + "value": "9922955c3e130feb-LAX" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Tue, 21 Oct 2025 17:48:01 GMT" + }, + { + "name": "etag", + "value": "W/\"44-8YrcNMtDwHD33MTo1ldKYcVY7RM\"" + }, + { + "name": "ratelimit-limit", + "value": "20" + }, + { + "name": "ratelimit-policy", + "value": "20;w=1" + }, + { + "name": "ratelimit-remaining", + "value": "19" + }, + { + "name": "ratelimit-reset", + "value": "1" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "headersSize": 370, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 404, + "statusText": "Not Found" + }, + "startedDateTime": "2025-10-21T17:48:01.254Z", + "time": 133, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 133 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/src/segments/audiences.integration.spec.ts b/src/segments/audiences.integration.spec.ts new file mode 100644 index 00000000..1c193c51 --- /dev/null +++ b/src/segments/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 Segment', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.name).toBeTruthy(); + expect(result.data?.object).toBe('segment'); + 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('segment'); + } 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/segments/audiences.spec.ts similarity index 84% rename from src/audiences/audiences.spec.ts rename to src/segments/audiences.spec.ts index 1ff01153..ed076a64 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/segments/audiences.spec.ts @@ -1,24 +1,29 @@ +import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { - CreateAudienceOptions, - CreateAudienceResponseSuccess, -} from './interfaces/create-audience-options.interface'; -import type { GetAudienceResponseSuccess } from './interfaces/get-audience.interface'; -import type { ListAudiencesResponseSuccess } from './interfaces/list-audiences.interface'; -import type { RemoveAudiencesResponseSuccess } from './interfaces/remove-audience.interface'; + CreateSegmentOptions, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { GetSegmentResponseSuccess } from './interfaces/get-segment.interface'; +import type { ListSegmentsResponseSuccess } from './interfaces/list-segments.interface'; +import type { RemoveSegmentResponseSuccess } from './interfaces/remove-segment.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); describe('Audiences', () => { afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); describe('create', () => { it('creates a audience', async () => { - const payload: CreateAudienceOptions = { name: 'resend.com' }; - const response: CreateAudienceResponseSuccess = { + const payload: CreateSegmentOptions = { name: 'resend.com' }; + const response: CreateSegmentResponseSuccess = { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', name: 'Resend', - object: 'audience', + object: 'segment', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -37,7 +42,7 @@ describe('Audiences', () => { "data": { "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", "name": "Resend", - "object": "audience", + "object": "segment", }, "error": null, } @@ -45,7 +50,7 @@ describe('Audiences', () => { }); it('throws error when missing name', async () => { - const payload: CreateAudienceOptions = { name: '' }; + const payload: CreateSegmentOptions = { name: '' }; const response: ErrorResponse = { name: 'missing_required_field', message: 'Missing "name" field', @@ -76,7 +81,7 @@ describe('Audiences', () => { }); describe('list', () => { - const response: ListAudiencesResponseSuccess = { + const response: ListSegmentsResponseSuccess = { object: 'list', has_more: false, data: [ @@ -108,7 +113,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences', + 'https://api.resend.com/segments', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -131,7 +136,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1', + 'https://api.resend.com/segments?limit=1', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -155,7 +160,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1&after=cursor-value', + 'https://api.resend.com/segments?limit=1&after=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -179,7 +184,7 @@ describe('Audiences', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/audiences?limit=1&before=cursor-value', + 'https://api.resend.com/segments?limit=1&before=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -222,8 +227,8 @@ describe('Audiences', () => { }); it('get audience', async () => { - const response: GetAudienceResponseSuccess = { - object: 'audience', + const response: GetSegmentResponseSuccess = { + object: 'segment', id: 'fd61172c-cafc-40f5-b049-b45947779a29', name: 'resend.com', created_at: '2023-06-21T06:10:36.144Z', @@ -247,7 +252,7 @@ describe('Audiences', () => { "created_at": "2023-06-21T06:10:36.144Z", "id": "fd61172c-cafc-40f5-b049-b45947779a29", "name": "resend.com", - "object": "audience", + "object": "segment", }, "error": null, } @@ -258,8 +263,8 @@ describe('Audiences', () => { describe('remove', () => { it('removes a audience', async () => { const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; - const response: RemoveAudiencesResponseSuccess = { - object: 'audience', + const response: RemoveSegmentResponseSuccess = { + object: 'segment', id, deleted: true, }; @@ -278,7 +283,7 @@ describe('Audiences', () => { "data": { "deleted": true, "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", - "object": "audience", + "object": "segment", }, "error": null, } diff --git a/src/segments/interfaces/create-segment-options.interface.ts b/src/segments/interfaces/create-segment-options.interface.ts new file mode 100644 index 00000000..da9fa0cc --- /dev/null +++ b/src/segments/interfaces/create-segment-options.interface.ts @@ -0,0 +1,24 @@ +import type { PostOptions } from '../../common/interfaces'; +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface CreateSegmentOptions { + name: string; +} + +export interface CreateSegmentRequestOptions extends PostOptions {} + +export interface CreateSegmentResponseSuccess + extends Pick { + object: 'segment'; +} + +export type CreateSegmentResponse = + | { + data: CreateSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/segments/interfaces/get-segment.interface.ts b/src/segments/interfaces/get-segment.interface.ts new file mode 100644 index 00000000..0f22aab2 --- /dev/null +++ b/src/segments/interfaces/get-segment.interface.ts @@ -0,0 +1,17 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface GetSegmentResponseSuccess + extends Pick { + object: 'segment'; +} + +export type GetSegmentResponse = + | { + data: GetSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/segments/interfaces/index.ts b/src/segments/interfaces/index.ts new file mode 100644 index 00000000..c572db5d --- /dev/null +++ b/src/segments/interfaces/index.ts @@ -0,0 +1,5 @@ +export * from './create-segment-options.interface'; +export * from './get-segment.interface'; +export * from './list-segments.interface'; +export * from './remove-segment.interface'; +export * from './segment'; diff --git a/src/audiences/interfaces/list-audiences.interface.ts b/src/segments/interfaces/list-segments.interface.ts similarity index 51% rename from src/audiences/interfaces/list-audiences.interface.ts rename to src/segments/interfaces/list-segments.interface.ts index 54727fea..17f9e99a 100644 --- a/src/audiences/interfaces/list-audiences.interface.ts +++ b/src/segments/interfaces/list-segments.interface.ts @@ -1,18 +1,18 @@ import type { PaginationOptions } from '../../common/interfaces'; import type { ErrorResponse } from '../../interfaces'; -import type { Audience } from './audience'; +import type { Segment } from './segment'; -export type ListAudiencesOptions = PaginationOptions; +export type ListSegmentsOptions = PaginationOptions; -export type ListAudiencesResponseSuccess = { +export type ListSegmentsResponseSuccess = { object: 'list'; - data: Audience[]; + data: Segment[]; has_more: boolean; }; -export type ListAudiencesResponse = +export type ListSegmentsResponse = | { - data: ListAudiencesResponseSuccess; + data: ListSegmentsResponseSuccess; error: null; } | { diff --git a/src/segments/interfaces/remove-segment.interface.ts b/src/segments/interfaces/remove-segment.interface.ts new file mode 100644 index 00000000..887ded12 --- /dev/null +++ b/src/segments/interfaces/remove-segment.interface.ts @@ -0,0 +1,17 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Segment } from './segment'; + +export interface RemoveSegmentResponseSuccess extends Pick { + object: 'segment'; + deleted: boolean; +} + +export type RemoveSegmentResponse = + | { + data: RemoveSegmentResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/interfaces/audience.ts b/src/segments/interfaces/segment.ts similarity index 65% rename from src/audiences/interfaces/audience.ts rename to src/segments/interfaces/segment.ts index 7335960d..08a014a3 100644 --- a/src/audiences/interfaces/audience.ts +++ b/src/segments/interfaces/segment.ts @@ -1,4 +1,4 @@ -export interface Audience { +export interface Segment { created_at: string; id: string; name: string; diff --git a/src/segments/segments.integration.spec.ts b/src/segments/segments.integration.spec.ts new file mode 100644 index 00000000..7fbe0f73 --- /dev/null +++ b/src/segments/segments.integration.spec.ts @@ -0,0 +1,151 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: test code */ +import type { Polly } from '@pollyjs/core'; +import { Resend } from '../resend'; +import { setupPolly } from '../test-utils/polly-setup'; + +describe('Segments Integration Tests', () => { + let polly: Polly; + let resend: Resend; + + beforeEach(() => { + polly = setupPolly(); + resend = new Resend(process.env.RESEND_API_KEY || 're_fake_key'); + }); + + afterEach(async () => { + await polly.stop(); + }); + + describe('create', () => { + it('creates a segment', async () => { + const result = await resend.segments.create({ + name: 'Test Segment', + }); + + expect(result.data?.id).toBeTruthy(); + expect(result.data?.name).toBeTruthy(); + expect(result.data?.object).toBe('segment'); + const segmentId = result.data!.id; + + const removeResult = await resend.segments.remove(segmentId); + expect(removeResult.data?.deleted).toBe(true); + }); + + it('handles validation errors', async () => { + // @ts-expect-error: Testing invalid input + const result = await resend.segments.create({}); + + expect(result.error?.name).toBe('missing_required_field'); + }); + }); + + // Needs to be run with an account that can have multiple segments + describe.todo('list', () => { + it('lists segments without pagination', async () => { + const segmentIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.segments.create({ + name: `Test segment ${i} for listing`, + }); + + expect(createResult.data?.id).toBeTruthy(); + segmentIds.push(createResult.data!.id); + } + + const result = await resend.segments.list(); + + expect(result.data?.object).toBe('list'); + expect(result.data?.data.length).toBeGreaterThanOrEqual(6); + expect(result.data?.has_more).toBe(false); + } finally { + for (const id of segmentIds) { + const removeResult = await resend.segments.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + + it('lists segments with limit', async () => { + const segmentIds: string[] = []; + + try { + for (let i = 0; i < 6; i++) { + const createResult = await resend.segments.create({ + name: `Test segment ${i} for listing with limit`, + }); + + expect(createResult.data?.id).toBeTruthy(); + segmentIds.push(createResult.data!.id); + } + + const result = await resend.segments.list({ limit: 5 }); + + expect(result.data?.data.length).toBe(5); + expect(result.data?.has_more).toBe(true); + } finally { + for (const id of segmentIds) { + const removeResult = await resend.segments.remove(id); + expect(removeResult.data?.deleted).toBe(true); + } + } + }); + }); + + describe('get', () => { + it('retrieves a segment by id', async () => { + const createResult = await resend.segments.create({ + name: 'Test Segment for Get', + }); + + expect(createResult.data?.id).toBeTruthy(); + const segmentId = createResult.data!.id; + + try { + const getResult = await resend.segments.get(segmentId); + + expect(getResult.data?.id).toBe(segmentId); + expect(getResult.data?.name).toBe('Test Segment for Get'); + expect(getResult.data?.object).toBe('segment'); + } finally { + const removeResult = await resend.segments.remove(segmentId); + expect(removeResult.data?.deleted).toBe(true); + } + }); + + it('returns error for non-existent segment', async () => { + const result = await resend.segments.get( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.error?.name).toBe('not_found'); + }); + }); + + describe('remove', () => { + it('removes a segment', async () => { + const createResult = await resend.segments.create({ + name: 'Test Segment to Remove', + }); + + expect(createResult.data?.id).toBeTruthy(); + const segmentId = createResult.data!.id; + + const removeResult = await resend.segments.remove(segmentId); + + expect(removeResult.data?.deleted).toBe(true); + + const getResult = await resend.segments.get(segmentId); + expect(getResult.error?.name).toBe('not_found'); + }); + + it('appears to remove a segment that never existed', async () => { + const result = await resend.segments.remove( + '00000000-0000-0000-0000-000000000000', + ); + + expect(result.data?.deleted).toBe(true); + }); + }); +}); diff --git a/src/segments/segments.spec.ts b/src/segments/segments.spec.ts new file mode 100644 index 00000000..92d79f0f --- /dev/null +++ b/src/segments/segments.spec.ts @@ -0,0 +1,291 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { + CreateSegmentOptions, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { GetSegmentResponseSuccess } from './interfaces/get-segment.interface'; +import type { ListSegmentsResponseSuccess } from './interfaces/list-segments.interface'; +import type { RemoveSegmentResponseSuccess } from './interfaces/remove-segment.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('Segments', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a segment', async () => { + const payload: CreateSegmentOptions = { name: 'resend.com' }; + const response: CreateSegmentResponseSuccess = { + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + name: 'Resend', + object: 'segment', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.segments.create(payload), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", + "name": "Resend", + "object": "segment", + }, + "error": null, +} +`); + }); + + it('throws error when missing name', async () => { + const payload: CreateSegmentOptions = { name: '' }; + const response: ErrorResponse = { + name: 'missing_required_field', + message: 'Missing "name" field', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.segments.create(payload); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing "name" field", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('list', () => { + const response: ListSegmentsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + name: 'resend.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + name: 'react.email', + created_at: '2023-04-07T23:13:20.417116+00:00', + }, + ], + }; + + describe('when no pagination options are provided', () => { + it('lists audiences', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.segments.list(); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('when pagination options are provided', () => { + it('passes limit param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ limit: 1 }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes after param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ + limit: 1, + after: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1&after=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes before param and returns a response', async () => { + mockSuccessResponse(response, { + headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.segments.list({ + limit: 1, + before: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/segments?limit=1&before=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + }); + + describe('get', () => { + describe('when audience not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + message: 'Audience not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.segments.get('1234'); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Audience not found", + "name": "not_found", + }, +} +`); + }); + }); + + it('get audience', async () => { + const response: GetSegmentResponseSuccess = { + object: 'segment', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'resend.com', + created_at: '2023-06-21T06:10:36.144Z', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.segments.get('1234')).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2023-06-21T06:10:36.144Z", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "resend.com", + "object": "segment", + }, + "error": null, +} +`); + }); + }); + + describe('remove', () => { + it('removes a audience', async () => { + const id = '5262504e-8ed7-4fac-bd16-0d4be94bc9f2'; + const response: RemoveSegmentResponseSuccess = { + object: 'segment', + id, + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.segments.remove(id)).resolves.toMatchInlineSnapshot(` +{ + "data": { + "deleted": true, + "id": "5262504e-8ed7-4fac-bd16-0d4be94bc9f2", + "object": "segment", + }, + "error": null, +} +`); + }); + }); +}); diff --git a/src/segments/segments.ts b/src/segments/segments.ts new file mode 100644 index 00000000..5db4fce6 --- /dev/null +++ b/src/segments/segments.ts @@ -0,0 +1,59 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + CreateSegmentOptions, + CreateSegmentRequestOptions, + CreateSegmentResponse, + CreateSegmentResponseSuccess, +} from './interfaces/create-segment-options.interface'; +import type { + GetSegmentResponse, + GetSegmentResponseSuccess, +} from './interfaces/get-segment.interface'; +import type { + ListSegmentsOptions, + ListSegmentsResponse, + ListSegmentsResponseSuccess, +} from './interfaces/list-segments.interface'; +import type { + RemoveSegmentResponse, + RemoveSegmentResponseSuccess, +} from './interfaces/remove-segment.interface'; + +export class Segments { + constructor(private readonly resend: Resend) {} + + async create( + payload: CreateSegmentOptions, + options: CreateSegmentRequestOptions = {}, + ): Promise { + const data = await this.resend.post( + '/segments', + payload, + options, + ); + return data; + } + + async list(options: ListSegmentsOptions = {}): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/segments?${queryString}` : '/segments'; + + const data = await this.resend.get(url); + return data; + } + + async get(id: string): Promise { + const data = await this.resend.get( + `/segments/${id}`, + ); + return data; + } + + async remove(id: string): Promise { + const data = await this.resend.delete( + `/segments/${id}`, + ); + return data; + } +} 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..e05fe5b0 --- /dev/null +++ b/src/templates/interfaces/create-template-options.interface.ts @@ -0,0 +1,48 @@ +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 } from './template'; + +type TemplateContentCreationOptions = RequireAtLeastOne<{ + html: string; + react: React.ReactNode; +}>; + +type TemplateVariableCreationOptions = Pick & + ( + | { + type: 'string'; + fallbackValue?: string | null; + } + | { + type: 'number'; + fallbackValue?: number | null; + } + ); + +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..af36e42a --- /dev/null +++ b/src/templates/interfaces/template.ts @@ -0,0 +1,25 @@ +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 interface TemplateVariable { + key: string; + 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 new file mode 100644 index 00000000..c3b6708b --- /dev/null +++ b/src/templates/interfaces/update-template.interface.ts @@ -0,0 +1,36 @@ +import type { ErrorResponse } from '../../interfaces'; +import type { Template, TemplateVariable } from './template'; + +type TemplateVariableUpdateOptions = Pick & + ( + | { + type: 'string'; + fallbackValue?: string | null; + } + | { + type: 'number'; + fallbackValue?: number | null; + } + ); + +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..0f85d384 --- /dev/null +++ b/src/templates/templates.spec.ts @@ -0,0 +1,933 @@ +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`', + ); + }); + }); + + 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", + }, + } + `); + }); + }); + + 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); + + // 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 after and limit pagination parameters are included + const [secondUrl] = fetchMock.mock.calls[1]; + const secondParsedUrl = new URL(secondUrl as string); + + expect(secondParsedUrl.pathname).toBe('/templates'); + expect(secondParsedUrl.searchParams.get('after')).toBe('cursor2'); + expect(secondParsedUrl.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/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/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..62498b3d --- /dev/null +++ b/src/topics/topics.spec.ts @@ -0,0 +1,343 @@ +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.', + statusCode: 422, + }; + + 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", + "statusCode": 422, + }, +} +`); + }); + + 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.', + statusCode: 422, + }; + + 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", + "statusCode": 422, + }, +} +`); + }); + }); + + 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', + statusCode: 404, + }; + + 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", + "statusCode": 404, + }, +} +`); + }); + }); + + 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", + "statusCode": null, + }, +} +`); + }); + }); + + 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", + "statusCode": null, + }, +} +`); + }); + }); + + 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", + "statusCode": null, + }, +} +`); + }); + }); +}); diff --git a/src/topics/topics.ts b/src/topics/topics.ts new file mode 100644 index 00000000..46d7a16c --- /dev/null +++ b/src/topics/topics.ts @@ -0,0 +1,101 @@ +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.', + statusCode: null, + 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.', + statusCode: null, + 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.', + statusCode: null, + name: 'missing_required_field', + }, + }; + } + + const data = await this.resend.delete( + `/topics/${id}`, + ); + + return data; + } +} diff --git a/tsconfig.json b/tsconfig.json index 81e59ea5..bd5290f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ "target": "es2017", "types": ["vitest/globals"] }, - "include": ["src"] + "include": ["src", "integrations"] } 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, +});