feat: add webhooks (#334)

This commit is contained in:
KM Koushik
2026-01-18 20:50:54 +11:00
committed by GitHub
parent f40a311cc9
commit 8676965019
58 changed files with 5334 additions and 245 deletions
+10
View File
@@ -0,0 +1,10 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@usesend/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.lint.json",
tsconfigRootDir: __dirname,
},
};
View File
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@usesend/lib",
"version": "0.0.0",
"private": true,
"main": "./index.ts",
"types": "./index.ts",
"files": [
"src"
],
"scripts": {
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"@types/node": "^22.15.2",
"@usesend/eslint-config": "workspace:*",
"@usesend/typescript-config": "workspace:*",
"eslint": "^8.57.1",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
}
+57
View File
@@ -0,0 +1,57 @@
export const DELIVERY_DELAY_ERRORS = {
InternalFailure: "An internal useSend issue caused the message to be delayed.",
General: "A generic failure occurred during the SMTP conversation.",
MailboxFull:
"The recipient's mailbox is full and is unable to receive additional messages.",
SpamDetected:
"The recipient's mail server has detected a large amount of unsolicited email from your account.",
RecipientServerError:
"A temporary issue with the recipient's email server is preventing the delivery of the message.",
IPFailure:
"The IP address that's sending the message is being blocked or throttled by the recipient's email provider.",
TransientCommunicationFailure:
"There was a temporary communication failure during the SMTP conversation with the recipient's email provider.",
BYOIPHostNameLookupUnavailable:
"useSend was unable to look up the DNS hostname for your IP addresses. This type of delay only occurs when you use Bring Your Own IP.",
Undetermined:
"useSend wasn't able to determine the reason for the delivery delay.",
SendingDeferral:
"useSend has deemed it appropriate to internally defer the message.",
};
export const BOUNCE_ERROR_MESSAGES = {
Undetermined: "useSend was unable to determine a specific bounce reason.",
Permanent: {
General:
"useSend received a general hard bounce. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
NoEmail:
"useSend received a permanent hard bounce because the target email address does not exist. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
Suppressed:
"useSend has suppressed sending to this address because it has a recent history of bouncing as an invalid address. To override the global suppression list, see Using the useSend account-level suppression list.",
OnAccountSuppressionList:
"useSend has suppressed sending to this address because it is on the account-level suppression list. This does not count toward your bounce rate metric.",
},
Transient: {
General:
"useSend received a general bounce. You may be able to successfully send to this recipient in the future.",
MailboxFull:
"useSend received a mailbox full bounce. You may be able to successfully send to this recipient in the future.",
MessageTooLarge:
"useSend received a message too large bounce. You may be able to successfully send to this recipient if you reduce the size of the message.",
ContentRejected:
"useSend received a content rejected bounce. You may be able to successfully send to this recipient if you change the content of the message.",
AttachmentRejected:
"useSend received an attachment rejected bounce. You may be able to successfully send to this recipient if you remove or change the attachment.",
},
};
export const COMPLAINT_ERROR_MESSAGES = {
abuse: "Indicates unsolicited email or some other kind of email abuse.",
"auth-failure": "Email authentication failure report.",
fraud: "Indicates some kind of fraud or phishing activity.",
"not-spam":
"Indicates that the entity providing the report does not consider the message to be spam. This may be used to correct a message that was incorrectly tagged or categorized as spam.",
other:
"Indicates any other feedback that does not fit into other registered types.",
virus: "Reports that a virus is found in the originating message.",
};
+22
View File
@@ -0,0 +1,22 @@
export function invariant(
condition: unknown,
message = "Invariant failed"
): asserts condition {
if (!condition) {
throw new Error(message);
}
}
export function assertUnreachable(value: never): never {
throw new Error(`Reached unreachable code with value: ${String(value)}`);
}
export const isDefined = <T>(
value: T | null | undefined
): value is T => value !== null && value !== undefined;
export {
BOUNCE_ERROR_MESSAGES,
COMPLAINT_ERROR_MESSAGES,
DELIVERY_DELAY_ERRORS,
} from "./constants/ses-errors";
+214
View File
@@ -0,0 +1,214 @@
export const ContactEvents = [
"contact.created",
"contact.updated",
"contact.deleted",
] as const;
export type ContactWebhookEventType = (typeof ContactEvents)[number];
export const DomainEvents = [
"domain.created",
"domain.verified",
"domain.updated",
"domain.deleted",
] as const;
export type DomainWebhookEventType = (typeof DomainEvents)[number];
export const EmailEvents = [
"email.queued",
"email.sent",
"email.delivery_delayed",
"email.delivered",
"email.bounced",
"email.rejected",
"email.rendering_failure",
"email.complained",
"email.failed",
"email.cancelled",
"email.suppressed",
"email.opened",
"email.clicked",
] as const;
export type EmailWebhookEventType = (typeof EmailEvents)[number];
export const WebhookTestEvents = ["webhook.test"] as const;
export type WebhookTestEventType = (typeof WebhookTestEvents)[number];
export const WebhookEvents = [
...ContactEvents,
...DomainEvents,
...EmailEvents,
...WebhookTestEvents,
] as const;
export type WebhookEventType = (typeof WebhookEvents)[number];
export type EmailStatus =
| "QUEUED"
| "SENT"
| "DELIVERY_DELAYED"
| "DELIVERED"
| "BOUNCED"
| "REJECTED"
| "RENDERING_FAILURE"
| "COMPLAINED"
| "FAILED"
| "CANCELLED"
| "SUPPRESSED"
| "OPENED"
| "CLICKED"
| "SCHEDULED";
export type EmailBasePayload = {
id: string;
status: EmailStatus;
from: string;
to: Array<string>;
occurredAt: string;
campaignId?: string | null;
contactId?: string | null;
domainId?: number | null;
subject?: string;
templateId?: string;
metadata?: Record<string, unknown>;
};
export type ContactPayload = {
id: string;
email: string;
contactBookId: string;
subscribed: boolean;
properties: Record<string, unknown>;
firstName?: string | null;
lastName?: string | null;
createdAt: string;
updatedAt: string;
};
export type DomainPayload = {
id: number;
name: string;
status: string;
region: string;
createdAt: string;
updatedAt: string;
clickTracking: boolean;
openTracking: boolean;
subdomain?: string | null;
sesTenantId?: string | null;
dkimStatus?: string | null;
spfDetails?: string | null;
dmarcAdded?: boolean | null;
};
export type EmailBouncedPayload = EmailBasePayload & {
bounce: {
type: "Transient" | "Permanent" | "Undetermined";
subType:
| "General"
| "NoEmail"
| "Suppressed"
| "OnAccountSuppressionList"
| "MailboxFull"
| "MessageTooLarge"
| "ContentRejected"
| "AttachmentRejected";
message?: string;
};
};
export type EmailFailedPayload = EmailBasePayload & {
failed: {
reason: string;
};
};
export type EmailSuppressedPayload = EmailBasePayload & {
suppression: {
type: "Bounce" | "Complaint" | "Manual";
reason: string;
source?: string;
};
};
export type EmailOpenedPayload = EmailBasePayload & {
open: {
timestamp: string;
userAgent?: string;
ip?: string;
platform?: string;
};
};
export type EmailClickedPayload = EmailBasePayload & {
click: {
timestamp: string;
url: string;
userAgent?: string;
ip?: string;
platform?: string;
};
};
export type WebhookTestPayload = {
test: boolean;
webhookId: string;
sentAt: string;
};
export type EmailEventPayloadMap = {
"email.queued": EmailBasePayload;
"email.sent": EmailBasePayload;
"email.delivery_delayed": EmailBasePayload;
"email.delivered": EmailBasePayload;
"email.bounced": EmailBouncedPayload;
"email.rejected": EmailBasePayload;
"email.rendering_failure": EmailBasePayload;
"email.complained": EmailBasePayload;
"email.failed": EmailFailedPayload;
"email.cancelled": EmailBasePayload;
"email.suppressed": EmailSuppressedPayload;
"email.opened": EmailOpenedPayload;
"email.clicked": EmailClickedPayload;
};
export type DomainEventPayloadMap = {
"domain.created": DomainPayload;
"domain.verified": DomainPayload;
"domain.updated": DomainPayload;
"domain.deleted": DomainPayload;
};
export type ContactEventPayloadMap = {
"contact.created": ContactPayload;
"contact.updated": ContactPayload;
"contact.deleted": ContactPayload;
};
export type WebhookTestEventPayloadMap = {
"webhook.test": WebhookTestPayload;
};
export type WebhookEventPayloadMap = EmailEventPayloadMap &
DomainEventPayloadMap &
ContactEventPayloadMap &
WebhookTestEventPayloadMap;
export type WebhookPayloadData<TType extends WebhookEventType> =
WebhookEventPayloadMap[TType];
export type WebhookEvent<TType extends WebhookEventType> = {
id: string;
type: TType;
createdAt: string;
data: WebhookPayloadData<TType>;
};
export type WebhookEventData = {
[T in WebhookEventType]: WebhookEvent<T>;
}[WebhookEventType];
export const WEBHOOK_EVENT_VERSION = "2026-01-18";
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@usesend/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@usesend/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "turbo", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}