@kitcn/resend 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +140 -0
- package/dist/index.js +125 -0
- package/package.json +2 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Plugin } from "kitcn/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/shared.d.ts
|
|
4
|
+
type Status = 'waiting' | 'queued' | 'cancelled' | 'sent' | 'delivered' | 'delivery_delayed' | 'bounced' | 'failed';
|
|
5
|
+
type Template = {
|
|
6
|
+
id: string;
|
|
7
|
+
variables?: Record<string, string | number>;
|
|
8
|
+
};
|
|
9
|
+
type RuntimeConfig = {
|
|
10
|
+
initialBackoffMs: number;
|
|
11
|
+
retryAttempts: number;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
testMode: boolean;
|
|
14
|
+
};
|
|
15
|
+
type Recipient = string | string[];
|
|
16
|
+
type Header$1 = {
|
|
17
|
+
name: string;
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
type CommonEventFields = {
|
|
21
|
+
broadcast_id?: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
email_id: string;
|
|
24
|
+
from: Recipient;
|
|
25
|
+
to: Recipient;
|
|
26
|
+
cc?: Recipient;
|
|
27
|
+
bcc?: Recipient;
|
|
28
|
+
reply_to?: Recipient;
|
|
29
|
+
headers?: Header$1[];
|
|
30
|
+
subject: string;
|
|
31
|
+
};
|
|
32
|
+
declare const ACCEPTED_EVENT_TYPES: readonly ["email.sent", "email.delivered", "email.bounced", "email.complained", "email.failed", "email.delivery_delayed", "email.opened", "email.clicked"];
|
|
33
|
+
type BaseEmailEvent<TType extends (typeof ACCEPTED_EVENT_TYPES)[number]> = {
|
|
34
|
+
type: TType;
|
|
35
|
+
created_at: string;
|
|
36
|
+
data: CommonEventFields;
|
|
37
|
+
};
|
|
38
|
+
type BouncedEmailEvent = BaseEmailEvent<'email.bounced'> & {
|
|
39
|
+
data: CommonEventFields & {
|
|
40
|
+
bounce: {
|
|
41
|
+
message: string;
|
|
42
|
+
subType: string;
|
|
43
|
+
type: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
type OpenedEmailEvent = BaseEmailEvent<'email.opened'> & {
|
|
48
|
+
data: CommonEventFields & {
|
|
49
|
+
open: {
|
|
50
|
+
ipAddress: string;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
userAgent: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
type ClickedEmailEvent = BaseEmailEvent<'email.clicked'> & {
|
|
57
|
+
data: CommonEventFields & {
|
|
58
|
+
click: {
|
|
59
|
+
ipAddress: string;
|
|
60
|
+
link: string;
|
|
61
|
+
timestamp: string;
|
|
62
|
+
userAgent: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
type FailedEmailEvent = BaseEmailEvent<'email.failed'> & {
|
|
67
|
+
data: CommonEventFields & {
|
|
68
|
+
failed: {
|
|
69
|
+
reason: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
type EmailEvent = BaseEmailEvent<'email.sent'> | BaseEmailEvent<'email.delivered'> | BaseEmailEvent<'email.delivery_delayed'> | BaseEmailEvent<'email.complained'> | BouncedEmailEvent | OpenedEmailEvent | ClickedEmailEvent | FailedEmailEvent;
|
|
74
|
+
type SendEmailOptions = {
|
|
75
|
+
from: string;
|
|
76
|
+
to: string | string[];
|
|
77
|
+
cc?: string | string[];
|
|
78
|
+
bcc?: string | string[];
|
|
79
|
+
subject: string;
|
|
80
|
+
html?: string;
|
|
81
|
+
text?: string;
|
|
82
|
+
replyTo?: string[];
|
|
83
|
+
headers?: {
|
|
84
|
+
name: string;
|
|
85
|
+
value: string;
|
|
86
|
+
}[];
|
|
87
|
+
} | {
|
|
88
|
+
from: string;
|
|
89
|
+
to: string | string[];
|
|
90
|
+
cc?: string | string[];
|
|
91
|
+
bcc?: string | string[];
|
|
92
|
+
subject?: string;
|
|
93
|
+
template: {
|
|
94
|
+
id: string;
|
|
95
|
+
variables?: Record<string, string | number>;
|
|
96
|
+
};
|
|
97
|
+
html?: never;
|
|
98
|
+
text?: never;
|
|
99
|
+
replyTo?: string[];
|
|
100
|
+
headers?: {
|
|
101
|
+
name: string;
|
|
102
|
+
value: string;
|
|
103
|
+
}[];
|
|
104
|
+
};
|
|
105
|
+
type EmailStatus = {
|
|
106
|
+
status: Status;
|
|
107
|
+
errorMessage: string | null;
|
|
108
|
+
bounced: boolean;
|
|
109
|
+
complained: boolean;
|
|
110
|
+
failed: boolean;
|
|
111
|
+
deliveryDelayed: boolean;
|
|
112
|
+
opened: boolean;
|
|
113
|
+
clicked: boolean;
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/runtime-helpers.d.ts
|
|
117
|
+
type Header = {
|
|
118
|
+
name: string;
|
|
119
|
+
value: string;
|
|
120
|
+
};
|
|
121
|
+
type ResendOptions = Partial<RuntimeConfig> & {
|
|
122
|
+
webhookSecret?: string;
|
|
123
|
+
};
|
|
124
|
+
type ResendApi = RuntimeConfig & {
|
|
125
|
+
webhookSecret: string;
|
|
126
|
+
verifyWebhookEvent(req: Request): Promise<EmailEvent>;
|
|
127
|
+
};
|
|
128
|
+
declare function getSegment(now: number): number;
|
|
129
|
+
declare function isTestEmail(email: string): boolean;
|
|
130
|
+
declare function normalizeRecipientList(value: string | string[] | undefined): string[];
|
|
131
|
+
declare function normalizeHeaders(headers: Header[] | undefined, idempotencyKey: string): Record<string, string>;
|
|
132
|
+
declare function shouldRetry(status: number, attempt: number, retryAttempts: number): boolean;
|
|
133
|
+
declare function getRetryDelayMs(initialBackoffMs: number, attempt: number): number;
|
|
134
|
+
declare function parseEmailEvent(value: unknown): EmailEvent | null;
|
|
135
|
+
declare function canUpgradeStatus(currentStatus: Status, nextStatus: Status): boolean;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/index.d.ts
|
|
138
|
+
declare const ResendPlugin: Plugin<'resend', ResendOptions, ResendApi>;
|
|
139
|
+
//#endregion
|
|
140
|
+
export { type EmailEvent, type EmailStatus, type ResendApi, type ResendOptions, ResendPlugin, type RuntimeConfig, type SendEmailOptions, type Status, type Template, canUpgradeStatus, getRetryDelayMs, getSegment, isTestEmail, normalizeHeaders, normalizeRecipientList, parseEmailEvent, shouldRetry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { definePlugin } from "kitcn/plugins";
|
|
2
|
+
import { Webhook } from "svix";
|
|
3
|
+
|
|
4
|
+
//#region src/shared.ts
|
|
5
|
+
const ACCEPTED_EVENT_TYPES = [
|
|
6
|
+
"email.sent",
|
|
7
|
+
"email.delivered",
|
|
8
|
+
"email.bounced",
|
|
9
|
+
"email.complained",
|
|
10
|
+
"email.failed",
|
|
11
|
+
"email.delivery_delayed",
|
|
12
|
+
"email.opened",
|
|
13
|
+
"email.clicked"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/runtime-helpers.ts
|
|
18
|
+
const SEGMENT_MS = 125;
|
|
19
|
+
const PERMANENT_ERROR_CODES = new Set([
|
|
20
|
+
400,
|
|
21
|
+
401,
|
|
22
|
+
403,
|
|
23
|
+
404,
|
|
24
|
+
405,
|
|
25
|
+
406,
|
|
26
|
+
407,
|
|
27
|
+
408,
|
|
28
|
+
410,
|
|
29
|
+
411,
|
|
30
|
+
413,
|
|
31
|
+
414,
|
|
32
|
+
415,
|
|
33
|
+
416,
|
|
34
|
+
418,
|
|
35
|
+
421,
|
|
36
|
+
422,
|
|
37
|
+
426,
|
|
38
|
+
427,
|
|
39
|
+
428,
|
|
40
|
+
431
|
|
41
|
+
]);
|
|
42
|
+
const RESEND_TEST_EMAIL_REGEX = /^(delivered|bounced|complained)(\+[a-zA-Z0-9_-]*)?@resend\.dev$/;
|
|
43
|
+
const EMAIL_STATUS_RANK = {
|
|
44
|
+
waiting: 0,
|
|
45
|
+
queued: 1,
|
|
46
|
+
sent: 2,
|
|
47
|
+
delivery_delayed: 3,
|
|
48
|
+
delivered: 4,
|
|
49
|
+
bounced: 5,
|
|
50
|
+
failed: 5,
|
|
51
|
+
cancelled: 100
|
|
52
|
+
};
|
|
53
|
+
function getSegment(now) {
|
|
54
|
+
return Math.floor(now / SEGMENT_MS);
|
|
55
|
+
}
|
|
56
|
+
function isTestEmail(email) {
|
|
57
|
+
return RESEND_TEST_EMAIL_REGEX.test(email);
|
|
58
|
+
}
|
|
59
|
+
function normalizeRecipientList(value) {
|
|
60
|
+
if (!value) return [];
|
|
61
|
+
return Array.isArray(value) ? value : [value];
|
|
62
|
+
}
|
|
63
|
+
function normalizeHeaders(headers, idempotencyKey) {
|
|
64
|
+
const merged = Object.fromEntries((headers ?? []).map((header) => [header.name, header.value]));
|
|
65
|
+
if (!Object.keys(merged).some((headerName) => headerName.toLowerCase() === "idempotency-key")) merged["Idempotency-Key"] = idempotencyKey;
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
function shouldRetry(status, attempt, retryAttempts) {
|
|
69
|
+
if (attempt >= retryAttempts) return false;
|
|
70
|
+
if (status === 429) return true;
|
|
71
|
+
if (status >= 500) return true;
|
|
72
|
+
return !PERMANENT_ERROR_CODES.has(status);
|
|
73
|
+
}
|
|
74
|
+
function getRetryDelayMs(initialBackoffMs, attempt) {
|
|
75
|
+
return initialBackoffMs * 2 ** attempt;
|
|
76
|
+
}
|
|
77
|
+
function isRecord(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
function parseEmailEvent(value) {
|
|
81
|
+
if (!isRecord(value)) return null;
|
|
82
|
+
const eventType = value.type;
|
|
83
|
+
const createdAt = value.created_at;
|
|
84
|
+
const eventData = value.data;
|
|
85
|
+
if (typeof eventType !== "string" || typeof createdAt !== "string" || !isRecord(eventData)) return null;
|
|
86
|
+
if (!ACCEPTED_EVENT_TYPES.includes(eventType)) return null;
|
|
87
|
+
if (typeof eventData.email_id !== "string") return null;
|
|
88
|
+
if (eventType === "email.bounced" && (!isRecord(eventData.bounce) || typeof eventData.bounce.message !== "string")) return null;
|
|
89
|
+
if (eventType === "email.failed" && (!isRecord(eventData.failed) || typeof eventData.failed.reason !== "string")) return null;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function canUpgradeStatus(currentStatus, nextStatus) {
|
|
93
|
+
if (currentStatus === "cancelled") return false;
|
|
94
|
+
return EMAIL_STATUS_RANK[nextStatus] > EMAIL_STATUS_RANK[currentStatus];
|
|
95
|
+
}
|
|
96
|
+
async function verifyResendWebhookEvent(req, webhookSecret) {
|
|
97
|
+
if (!webhookSecret) throw new Error("Webhook secret is not set");
|
|
98
|
+
const webhook = new Webhook(webhookSecret);
|
|
99
|
+
const raw = await req.text();
|
|
100
|
+
const parsed = parseEmailEvent(webhook.verify(raw, {
|
|
101
|
+
"svix-id": req.headers.get("svix-id") ?? "",
|
|
102
|
+
"svix-timestamp": req.headers.get("svix-timestamp") ?? "",
|
|
103
|
+
"svix-signature": req.headers.get("svix-signature") ?? ""
|
|
104
|
+
}));
|
|
105
|
+
if (!parsed) throw new Error("Invalid Resend webhook payload");
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/index.ts
|
|
111
|
+
/** biome-ignore-all lint/performance/noBarrelFile: package entry */
|
|
112
|
+
const ResendPlugin = definePlugin("resend", ({ options }) => {
|
|
113
|
+
const webhookSecret = options?.webhookSecret ?? "";
|
|
114
|
+
return {
|
|
115
|
+
apiKey: options?.apiKey ?? "",
|
|
116
|
+
webhookSecret,
|
|
117
|
+
initialBackoffMs: options?.initialBackoffMs ?? 3e4,
|
|
118
|
+
retryAttempts: options?.retryAttempts ?? 5,
|
|
119
|
+
testMode: options?.testMode ?? true,
|
|
120
|
+
verifyWebhookEvent: (req) => verifyResendWebhookEvent(req, webhookSecret)
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
export { ResendPlugin, canUpgradeStatus, getRetryDelayMs, getSegment, isTestEmail, normalizeHeaders, normalizeRecipientList, parseEmailEvent, shouldRetry };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitcn/resend",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
4
4
|
"description": "kitcn Resend plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsdown",
|
|
16
|
+
"prepack": "bun run build",
|
|
16
17
|
"typecheck": "tsc --noEmit"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|