feat: add webhooks (#334)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@usesend/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@usesend/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "turbo", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user