@cms0/transactional 0.2.19 → 0.2.21

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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @cms0/transactional
2
2
 
3
- Transactional email package using [Plunk](https://useplunk.com) and [React Email](https://react.email).
3
+ Transactional email package with provider-agnostic transports and React Email templates.
4
4
 
5
5
  ## Installation
6
6
 
7
- This package is part of the workspace. Install dependencies from the root:
7
+ This package is part of the package graph. Install dependencies from the root:
8
8
 
9
9
  ```bash
10
10
  pnpm install
@@ -12,10 +12,14 @@ pnpm install
12
12
 
13
13
  ## Configuration
14
14
 
15
- Set your Plunk API key as an environment variable:
15
+ Configure one of the supported transports in the consuming app:
16
16
 
17
17
  ```bash
18
- PLUNK_API_KEY=sk_your_api_key_here
18
+ CMS0_EMAIL_TRANSPORT=log
19
+ # or:
20
+ CMS0_EMAIL_TRANSPORT=smtp
21
+ # or:
22
+ CMS0_EMAIL_TRANSPORT=plunk
19
23
  ```
20
24
 
21
25
  ## Usage
@@ -124,13 +128,17 @@ Send a password reset email.
124
128
 
125
129
  ## Advanced Usage
126
130
 
127
- ### Custom Plunk Client
131
+ ### Custom Email Service
128
132
 
129
- You can create and set a custom Plunk client:
133
+ You can create and set a custom default email service:
130
134
 
131
135
  ```typescript
132
- import { PlunkClient, setDefaultClient } from '@cms0/transactional';
136
+ import { createEmailService, setDefaultEmailService } from '@cms0/transactional';
133
137
 
134
- const customClient = new PlunkClient('sk_custom_api_key');
135
- setDefaultClient(customClient);
138
+ const customService = createEmailService({
139
+ transport: { kind: 'log' },
140
+ defaultFrom: 'no-reply@example.com',
141
+ });
142
+
143
+ setDefaultEmailService(customService);
136
144
  ```
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = ResetPasswordEmail;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const components_1 = require("@react-email/components");
6
- function ResetPasswordEmail({ resetUrl = "https://example.com/reset-password", userName = "there", }) {
7
- return ((0, jsx_runtime_1.jsxs)(components_1.Html, { lang: "en", children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsx)(components_1.Body, { style: main, children: (0, jsx_runtime_1.jsx)(components_1.Container, { style: container, children: (0, jsx_runtime_1.jsxs)(components_1.Section, { style: content, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: heading, children: "Reset your password" }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: paragraph, children: ["Hi ", userName, ","] }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "We received a request to reset your password. Click the button below to create a new password:" }), (0, jsx_runtime_1.jsx)(components_1.Button, { href: resetUrl, style: button, children: "Reset Password" }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: hr }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "This link will expire in 1 hour for security reasons." }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: footer, children: "If you didn't request a password reset, you can safely ignore this email. Your password will not be changed." })] }) }) })] }));
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Html, Head, Body, Container, Section, Text, Button, Hr, } from "@react-email/components";
3
+ export default function ResetPasswordEmail({ resetUrl = "https://example.com/reset-password", userName = "there", }) {
4
+ return (_jsxs(Html, { lang: "en", children: [_jsx(Head, {}), _jsx(Body, { style: main, children: _jsx(Container, { style: container, children: _jsxs(Section, { style: content, children: [_jsx(Text, { style: heading, children: "Reset your password" }), _jsxs(Text, { style: paragraph, children: ["Hi ", userName, ","] }), _jsx(Text, { style: paragraph, children: "We received a request to reset your password. Click the button below to create a new password:" }), _jsx(Button, { href: resetUrl, style: button, children: "Reset Password" }), _jsx(Hr, { style: hr }), _jsx(Text, { style: paragraph, children: "This link will expire in 1 hour for security reasons." }), _jsx(Text, { style: footer, children: "If you didn't request a password reset, you can safely ignore this email. Your password will not be changed." })] }) }) })] }));
8
5
  }
9
6
  const main = {
10
7
  backgroundColor: "#f6f9fc",
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = TeamInviteEmail;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const components_1 = require("@react-email/components");
6
- function TeamInviteEmail({ teamName = "Your Team", inviterName = "A team member", inviteUrl = "https://example.com/accept-invite", recipientEmail = "user@example.com", }) {
7
- return ((0, jsx_runtime_1.jsxs)(components_1.Html, { lang: "en", children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsx)(components_1.Body, { style: main, children: (0, jsx_runtime_1.jsx)(components_1.Container, { style: container, children: (0, jsx_runtime_1.jsxs)(components_1.Section, { style: content, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: heading, children: "You've been invited to join a team" }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: paragraph, children: [inviterName, " has invited you to join ", (0, jsx_runtime_1.jsx)("strong", { children: teamName }), "."] }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "Click the button below to accept the invitation and join the team:" }), (0, jsx_runtime_1.jsx)(components_1.Button, { href: inviteUrl, style: button, children: "Accept Invitation" }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: hr }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: footer, children: ["This invitation was sent to ", recipientEmail, ". If you weren't expecting this invitation, you can safely ignore this email."] })] }) }) })] }));
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Html, Head, Body, Container, Section, Text, Button, Hr, } from "@react-email/components";
3
+ export default function TeamInviteEmail({ teamName = "Your Team", inviterName = "A team member", inviteUrl = "https://example.com/accept-invite", recipientEmail = "user@example.com", }) {
4
+ return (_jsxs(Html, { lang: "en", children: [_jsx(Head, {}), _jsx(Body, { style: main, children: _jsx(Container, { style: container, children: _jsxs(Section, { style: content, children: [_jsx(Text, { style: heading, children: "You've been invited to join a team" }), _jsxs(Text, { style: paragraph, children: [inviterName, " has invited you to join ", _jsx("strong", { children: teamName }), "."] }), _jsx(Text, { style: paragraph, children: "Click the button below to accept the invitation and join the team:" }), _jsx(Button, { href: inviteUrl, style: button, children: "Accept Invitation" }), _jsx(Hr, { style: hr }), _jsxs(Text, { style: footer, children: ["This invitation was sent to ", recipientEmail, ". If you weren't expecting this invitation, you can safely ignore this email."] })] }) }) })] }));
8
5
  }
9
6
  const main = {
10
7
  backgroundColor: "#f6f9fc",
@@ -1,9 +1,8 @@
1
- import type { PlunkSendEmailRequest, PlunkSendEmailResponse } from "./types";
2
- export declare class PlunkClient {
3
- private apiKey;
4
- constructor(apiKey?: string);
5
- sendEmail(request: PlunkSendEmailRequest): Promise<PlunkSendEmailResponse>;
6
- }
7
- export declare function getDefaultClient(): PlunkClient;
8
- export declare function setDefaultClient(client: PlunkClient): void;
1
+ import type { EmailService, EmailServiceConfig, EmailTransport } from "./types.js";
2
+ import type { EmailTransportConfig } from "@cms0/shared";
3
+ export declare const createEmailTransport: (config: EmailTransportConfig) => EmailTransport;
4
+ export declare const createEmailService: (config: EmailServiceConfig) => EmailService;
5
+ export declare const getDefaultEmailService: () => EmailService;
6
+ export declare const setDefaultEmailService: (service: EmailService) => void;
7
+ export declare const clearDefaultEmailService: () => void;
9
8
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAI7E,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,CAAC,EAAE,MAAM;IAUrB,SAAS,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;CA2BjF;AAKD,wBAAgB,gBAAgB,IAAI,WAAW,CAK9C;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAE1D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAEV,oBAAoB,EACrB,MAAM,cAAc,CAAC;AA6KtB,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,cAWF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,kBAAkB,KACzB,YAYF,CAAC;AAIF,eAAO,MAAM,sBAAsB,oBAQlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,SAAS,YAAY,SAE3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,YAEpC,CAAC"}
@@ -1,50 +1,154 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlunkClient = void 0;
4
- exports.getDefaultClient = getDefaultClient;
5
- exports.setDefaultClient = setDefaultClient;
6
- const PLUNK_API_BASE_URL = "https://next-api.useplunk.com";
7
- class PlunkClient {
8
- apiKey;
9
- constructor(apiKey) {
10
- this.apiKey = apiKey || process.env.PLUNK_API_KEY || "";
11
- if (!this.apiKey) {
12
- throw new Error("Plunk API key is required. Set PLUNK_API_KEY environment variable or pass it to the constructor.");
13
- }
1
+ const DEFAULT_PLUNK_API_BASE_URL = "https://next-api.useplunk.com";
2
+ const importNodemailer = new Function("return import('nodemailer')");
3
+ const normalizeAddress = (value) => {
4
+ if (!value) {
5
+ return undefined;
6
+ }
7
+ if (typeof value === "string") {
8
+ return value;
14
9
  }
15
- async sendEmail(request) {
16
- try {
17
- const response = await fetch(`${PLUNK_API_BASE_URL}/v1/send`, {
18
- method: "POST",
19
- headers: {
20
- "Authorization": `Bearer ${this.apiKey}`,
21
- "Content-Type": "application/json",
22
- },
23
- body: JSON.stringify(request),
10
+ return value.name?.trim()
11
+ ? `${value.name.trim()} <${value.email}>`
12
+ : value.email;
13
+ };
14
+ const normalizeRecipients = (value) => Array.isArray(value)
15
+ ? value
16
+ .map((entry) => normalizeAddress(entry))
17
+ .filter((entry) => Boolean(entry))
18
+ : ensureEmailAddress(value, "Recipient");
19
+ const ensureEmailAddress = (value, label) => {
20
+ if (!value) {
21
+ throw new Error(`${label} email address is required.`);
22
+ }
23
+ return normalizeAddress(value);
24
+ };
25
+ const stringifyRecipients = (value) => {
26
+ const normalized = normalizeRecipients(value);
27
+ return Array.isArray(normalized) ? normalized.join(", ") : normalized;
28
+ };
29
+ const createLogEmailTransport = () => ({
30
+ kind: "log",
31
+ async send(message) {
32
+ console.info("[cms0/transactional:log]", JSON.stringify({
33
+ from: normalizeAddress(message.from),
34
+ provider: "log",
35
+ subject: message.subject,
36
+ to: stringifyRecipients(message.to),
37
+ }, null, 2));
38
+ return {
39
+ accepted: true,
40
+ messageId: null,
41
+ provider: "log",
42
+ };
43
+ },
44
+ });
45
+ const createSmtpEmailTransport = (config) => {
46
+ let transporter = null;
47
+ const getTransporter = async () => {
48
+ if (!transporter) {
49
+ const nodemailer = await importNodemailer();
50
+ const createTransport = nodemailer.default?.createTransport ?? nodemailer.createTransport;
51
+ const nextTransporter = createTransport({
52
+ auth: config.username || config.password
53
+ ? {
54
+ pass: config.password ?? undefined,
55
+ user: config.username ?? undefined,
56
+ }
57
+ : undefined,
58
+ host: config.host,
59
+ port: config.port,
60
+ secure: config.secure,
24
61
  });
25
- const data = await response.json();
26
- if (!response.ok) {
27
- throw new Error(data.error?.message || `Failed to send email: ${response.statusText}`);
28
- }
29
- return data;
62
+ transporter = nextTransporter;
63
+ return nextTransporter;
30
64
  }
31
- catch (error) {
32
- if (error instanceof Error) {
33
- throw error;
34
- }
35
- throw new Error("An unknown error occurred while sending email");
65
+ return transporter;
66
+ };
67
+ return {
68
+ kind: "smtp",
69
+ async send(message) {
70
+ const activeTransporter = await getTransporter();
71
+ const info = (await activeTransporter.sendMail({
72
+ attachments: message.attachments,
73
+ from: ensureEmailAddress(message.from, "Sender"),
74
+ headers: message.headers,
75
+ html: message.html,
76
+ replyTo: normalizeAddress(message.replyTo),
77
+ subject: message.subject,
78
+ text: message.text,
79
+ to: normalizeRecipients(message.to),
80
+ }));
81
+ return {
82
+ accepted: Boolean(info.accepted?.length ?? 0),
83
+ messageId: info.messageId ?? null,
84
+ provider: "smtp",
85
+ };
86
+ },
87
+ };
88
+ };
89
+ const createPlunkEmailTransport = (config) => ({
90
+ kind: "plunk",
91
+ async send(message) {
92
+ const response = await fetch(`${config.baseUrl ?? DEFAULT_PLUNK_API_BASE_URL}/v1/send`, {
93
+ method: "POST",
94
+ headers: {
95
+ Authorization: `Bearer ${config.apiKey}`,
96
+ "Content-Type": "application/json",
97
+ },
98
+ body: JSON.stringify({
99
+ body: message.html,
100
+ from: normalizeAddress(message.from),
101
+ headers: message.headers,
102
+ reply: normalizeAddress(message.replyTo),
103
+ subject: message.subject,
104
+ to: message.to,
105
+ }),
106
+ });
107
+ const payload = (await response.json().catch(() => null));
108
+ if (!response.ok) {
109
+ throw new Error(payload?.error?.message ||
110
+ `Failed to send email via Plunk: ${response.status} ${response.statusText}`);
36
111
  }
112
+ return {
113
+ accepted: Boolean(payload?.data?.emails?.length ?? 0),
114
+ messageId: null,
115
+ provider: "plunk",
116
+ };
117
+ },
118
+ });
119
+ export const createEmailTransport = (config) => {
120
+ switch (config.kind) {
121
+ case "log":
122
+ return createLogEmailTransport();
123
+ case "smtp":
124
+ return createSmtpEmailTransport(config);
125
+ case "plunk":
126
+ return createPlunkEmailTransport(config);
37
127
  }
38
- }
39
- exports.PlunkClient = PlunkClient;
40
- // Export a default instance
41
- let defaultClient = null;
42
- function getDefaultClient() {
43
- if (!defaultClient) {
44
- defaultClient = new PlunkClient();
128
+ throw new Error("Unsupported email transport configuration.");
129
+ };
130
+ export const createEmailService = (config) => {
131
+ const transport = createEmailTransport(config.transport);
132
+ return {
133
+ send(message) {
134
+ return transport.send({
135
+ ...message,
136
+ from: message.from ?? config.defaultFrom,
137
+ replyTo: message.replyTo ?? config.defaultReplyTo,
138
+ });
139
+ },
140
+ };
141
+ };
142
+ let defaultEmailService = null;
143
+ export const getDefaultEmailService = () => {
144
+ if (!defaultEmailService) {
145
+ throw new Error("Default email service has not been configured for @cms0/transactional.");
45
146
  }
46
- return defaultClient;
47
- }
48
- function setDefaultClient(client) {
49
- defaultClient = client;
50
- }
147
+ return defaultEmailService;
148
+ };
149
+ export const setDefaultEmailService = (service) => {
150
+ defaultEmailService = service;
151
+ };
152
+ export const clearDefaultEmailService = () => {
153
+ defaultEmailService = null;
154
+ };
@@ -1,13 +1,14 @@
1
- import { PlunkClient, setDefaultClient } from "./client";
2
- import type { TeamInviteProps, ResetPasswordProps, SendEmailOptions, PlunkSendEmailResponse } from "./types";
1
+ import { createEmailService, createEmailTransport, setDefaultEmailService, clearDefaultEmailService } from "./client.js";
2
+ import type { EmailService, EmailServiceConfig, EmailSendResult, EmailTransport, TeamInviteProps, ResetPasswordProps, SendEmailOptions } from "./types.js";
3
+ import type { EmailTransportConfig } from "@cms0/shared";
3
4
  /**
4
5
  * Send a team invitation email
5
6
  */
6
- export declare function sendTeamInvite(to: string, props: TeamInviteProps, options?: SendEmailOptions): Promise<PlunkSendEmailResponse>;
7
+ export declare function sendTeamInvite(to: string, props: TeamInviteProps, options?: SendEmailOptions): Promise<EmailSendResult>;
7
8
  /**
8
9
  * Send a password reset email
9
10
  */
10
- export declare function sendResetPassword(to: string, props: ResetPasswordProps, options?: SendEmailOptions): Promise<PlunkSendEmailResponse>;
11
- export type { TeamInviteProps, ResetPasswordProps, SendEmailOptions, PlunkSendEmailResponse, };
12
- export { PlunkClient, setDefaultClient };
11
+ export declare function sendResetPassword(to: string, props: ResetPasswordProps, options?: SendEmailOptions): Promise<EmailSendResult>;
12
+ export type { EmailService, EmailServiceConfig, EmailTransport, EmailTransportConfig, TeamInviteProps, ResetPasswordProps, SendEmailOptions, EmailSendResult, };
13
+ export { clearDefaultEmailService, createEmailService, createEmailTransport, setDefaultEmailService, };
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,WAAW,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAK3E,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,sBAAsB,CAAC,CAYjC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,sBAAsB,CAAC,CAYjC;AAGD,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,GACvB,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EAEpB,sBAAsB,EACtB,wBAAwB,EACzB,MAAM,aAAa,CAAC;AAKrB,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAKzD;;GAEG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAY1B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAY1B;AAGD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,GAChB,CAAC;AAEF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,CAAC"}
package/dist/src/index.js CHANGED
@@ -1,39 +1,34 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setDefaultClient = exports.PlunkClient = void 0;
4
- exports.sendTeamInvite = sendTeamInvite;
5
- exports.sendResetPassword = sendResetPassword;
6
- const client_1 = require("./client");
7
- Object.defineProperty(exports, "PlunkClient", { enumerable: true, get: function () { return client_1.PlunkClient; } });
8
- Object.defineProperty(exports, "setDefaultClient", { enumerable: true, get: function () { return client_1.setDefaultClient; } });
9
- const templates_1 = require("./templates");
1
+ import { createEmailService, createEmailTransport, getDefaultEmailService, setDefaultEmailService, clearDefaultEmailService, } from "./client.js";
2
+ import { renderTeamInvite, renderResetPassword, } from "./templates.js";
3
+ const getEmailService = (options) => options?.service ?? getDefaultEmailService();
10
4
  /**
11
5
  * Send a team invitation email
12
6
  */
13
- async function sendTeamInvite(to, props, options) {
14
- const client = (0, client_1.getDefaultClient)();
15
- const html = await (0, templates_1.renderTeamInvite)(props);
16
- return await client.sendEmail({
17
- to,
18
- subject: `Join ${props.teamName}`,
19
- body: html,
7
+ export async function sendTeamInvite(to, props, options) {
8
+ const service = getEmailService(options);
9
+ const html = await renderTeamInvite(props);
10
+ return service.send({
20
11
  from: options?.from,
21
- reply: options?.replyTo,
22
12
  headers: options?.headers,
13
+ html,
14
+ replyTo: options?.replyTo,
15
+ subject: `Join ${props.teamName}`,
16
+ to,
23
17
  });
24
18
  }
25
19
  /**
26
20
  * Send a password reset email
27
21
  */
28
- async function sendResetPassword(to, props, options) {
29
- const client = (0, client_1.getDefaultClient)();
30
- const html = await (0, templates_1.renderResetPassword)(props);
31
- return await client.sendEmail({
32
- to,
33
- subject: "Reset your password",
34
- body: html,
22
+ export async function sendResetPassword(to, props, options) {
23
+ const service = getEmailService(options);
24
+ const html = await renderResetPassword(props);
25
+ return service.send({
35
26
  from: options?.from,
36
- reply: options?.replyTo,
37
27
  headers: options?.headers,
28
+ html,
29
+ replyTo: options?.replyTo,
30
+ subject: "Reset your password",
31
+ to,
38
32
  });
39
33
  }
34
+ export { clearDefaultEmailService, createEmailService, createEmailTransport, setDefaultEmailService, };
@@ -1,4 +1,4 @@
1
- import type { TeamInviteProps, ResetPasswordProps } from "./types";
1
+ import type { TeamInviteProps, ResetPasswordProps } from "./types.js";
2
2
  export declare function renderTeamInvite(props: TeamInviteProps): Promise<string>;
3
3
  export declare function renderResetPassword(props: ResetPasswordProps): Promise<string>;
4
4
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/templates.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAE9E;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEpF"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/templates.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -1,17 +1,10 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.renderTeamInvite = renderTeamInvite;
7
- exports.renderResetPassword = renderResetPassword;
8
- const jsx_runtime_1 = require("react/jsx-runtime");
9
- const components_1 = require("@react-email/components");
10
- const team_invite_1 = __importDefault(require("../emails/team-invite"));
11
- const reset_password_1 = __importDefault(require("../emails/reset-password"));
12
- async function renderTeamInvite(props) {
13
- return await (0, components_1.render)((0, jsx_runtime_1.jsx)(team_invite_1.default, { ...props }));
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "@react-email/components";
3
+ import TeamInviteEmail from "../emails/team-invite.js";
4
+ import ResetPasswordEmail from "../emails/reset-password.js";
5
+ export async function renderTeamInvite(props) {
6
+ return await render(_jsx(TeamInviteEmail, { ...props }));
14
7
  }
15
- async function renderResetPassword(props) {
16
- return await (0, components_1.render)((0, jsx_runtime_1.jsx)(reset_password_1.default, { ...props }));
8
+ export async function renderResetPassword(props) {
9
+ return await render(_jsx(ResetPasswordEmail, { ...props }));
17
10
  }
@@ -1,43 +1,37 @@
1
- export interface PlunkSendEmailRequest {
2
- to: string | {
3
- name: string;
4
- email: string;
5
- } | Array<string | {
6
- name: string;
7
- email: string;
8
- }>;
9
- subject: string;
10
- body: string;
11
- from?: string | {
12
- name: string;
13
- email: string;
14
- };
15
- name?: string;
16
- subscribed?: boolean;
17
- data?: Record<string, unknown>;
1
+ import type { EmailAddress, EmailTransportConfig } from "@cms0/shared";
2
+ export type EmailAddressLike = string | EmailAddress;
3
+ export type EmailRecipient = EmailAddressLike | Array<EmailAddressLike>;
4
+ export interface EmailAttachment {
5
+ content: string;
6
+ contentType: string;
7
+ filename: string;
8
+ }
9
+ export interface EmailMessage {
10
+ attachments?: EmailAttachment[];
11
+ from?: EmailAddressLike;
18
12
  headers?: Record<string, string>;
19
- reply?: string;
20
- attachments?: Array<{
21
- filename: string;
22
- content: string;
23
- contentType: string;
24
- }>;
25
- }
26
- export interface PlunkSendEmailResponse {
27
- success: boolean;
28
- data?: {
29
- emails: Array<{
30
- contact: string;
31
- email: string;
32
- }>;
33
- timestamp: string;
34
- };
35
- error?: {
36
- code: string;
37
- error: string;
38
- message: string;
39
- timestamp: string;
40
- };
13
+ html: string;
14
+ replyTo?: EmailAddressLike;
15
+ subject: string;
16
+ text?: string;
17
+ to: EmailRecipient;
18
+ }
19
+ export interface EmailSendResult {
20
+ accepted: boolean;
21
+ messageId: string | null;
22
+ provider: EmailTransportConfig["kind"];
23
+ }
24
+ export interface EmailTransport {
25
+ readonly kind: EmailTransportConfig["kind"];
26
+ send(message: EmailMessage): Promise<EmailSendResult>;
27
+ }
28
+ export interface EmailService {
29
+ send(message: EmailMessage): Promise<EmailSendResult>;
30
+ }
31
+ export interface EmailServiceConfig {
32
+ defaultFrom?: EmailAddressLike;
33
+ defaultReplyTo?: EmailAddressLike;
34
+ transport: EmailTransportConfig;
41
35
  }
42
36
  export interface TeamInviteProps {
43
37
  teamName: string;
@@ -50,11 +44,9 @@ export interface ResetPasswordProps {
50
44
  userName?: string;
51
45
  }
52
46
  export interface SendEmailOptions {
53
- from?: string | {
54
- name: string;
55
- email: string;
56
- };
57
- replyTo?: string;
47
+ from?: EmailAddressLike;
48
+ replyTo?: EmailAddressLike;
49
+ service?: EmailService;
58
50
  headers?: Record<string, string>;
59
51
  }
60
52
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,KAAK,CAAC;YACZ,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC;SACf,CAAC,CAAC;QACH,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAGD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEvE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,CAAC;AAErD,MAAM,MAAM,cAAc,GACtB,gBAAgB,GAChB,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,cAAc,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAGD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC"}
package/dist/src/types.js CHANGED
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
package/package.json CHANGED
@@ -1,44 +1,50 @@
1
1
  {
2
2
  "name": "@cms0/transactional",
3
- "version": "0.2.19",
4
- "private": false,
3
+ "version": "0.2.21",
5
4
  "publishConfig": {
6
5
  "access": "restricted"
7
6
  },
8
- "main": "dist/src/index.js",
9
- "types": "./src/index.ts",
7
+ "type": "module",
8
+ "main": "./dist/src/index.js",
9
+ "types": "./dist/src/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.ts",
13
- "import": "./src/index.ts",
12
+ "types": "./dist/src/index.d.ts",
13
+ "import": "./dist/src/index.js",
14
14
  "require": "./dist/src/index.js",
15
- "default": "./src/index.ts"
15
+ "default": "./dist/src/index.js"
16
16
  },
17
17
  "./*": {
18
- "types": "./src/*.ts",
19
- "import": "./src/*.ts",
18
+ "types": "./dist/src/*.d.ts",
19
+ "import": "./dist/src/*.js",
20
20
  "require": "./dist/src/*.js"
21
21
  }
22
22
  },
23
+ "files": [
24
+ "dist"
25
+ ],
23
26
  "dependencies": {
24
- "@react-email/components": "0.0.31"
27
+ "@react-email/components": "^0.5.7",
28
+ "nodemailer": "^7.0.10",
29
+ "@cms0/shared": "0.2.21"
25
30
  },
26
31
  "peerDependencies": {
27
- "react": "^19.0.0",
28
- "react-dom": "^19.0.0"
32
+ "react": "^19.2.0",
33
+ "react-dom": "^19.2.0"
29
34
  },
30
35
  "devDependencies": {
31
- "@types/node": "^20.0.0",
32
- "@types/react": "^19.0.0",
33
- "@types/react-dom": "^19.0.0",
34
- "react": "^19.0.0",
35
- "react-dom": "^19.0.0",
36
- "typescript": "^5.0.0",
37
- "react-email": "3.0.3",
36
+ "@types/node": "^24.10.0",
37
+ "@types/nodemailer": "^7.0.3",
38
+ "@types/react": "^19.2.4",
39
+ "react": "19.2.0",
40
+ "typescript": "^5.9.3",
38
41
  "@cms0/typescript-config": "0.0.1"
39
42
  },
40
43
  "scripts": {
41
- "build": "tsc",
42
- "dev": "email dev"
44
+ "build": "tsc -p tsconfig.json",
45
+ "typecheck": "tsc --noEmit -p tsconfig.json",
46
+ "lint": "tsc --noEmit -p tsconfig.json",
47
+ "test:unit": "pnpm -C ../.. exec vitest run --config vitest.config.ts --project transactional-unit",
48
+ "test": "pnpm test:unit"
43
49
  }
44
50
  }
package/.env.example DELETED
@@ -1,3 +0,0 @@
1
- # Plunk API Key
2
- # Get your API key from https://app.useplunk.com/settings/api-keys
3
- PLUNK_API_KEY=sk_your_api_key_here
package/CHANGELOG.md DELETED
@@ -1,32 +0,0 @@
1
- # @cms0/transactional
2
-
3
- ## 0.2.19
4
-
5
- ## 0.2.18
6
-
7
- ## 0.2.17
8
-
9
- ## 0.2.16
10
-
11
- ## 0.2.15
12
-
13
- ## 0.2.14
14
-
15
- ### Patch Changes
16
-
17
- - e76b090: Republish the public runtime stack with a single compatibility boundary so
18
- Canvas, shared helpers, admin, and SDK packages cannot drift into broken
19
- published combinations.
20
-
21
- ## 0.0.3
22
-
23
- ### Patch Changes
24
-
25
- - d131093: Harden npm package publication by rebuilding tarballs during `prepack` and verifying packed artifacts in the main-branch release workflow. This fixes missing build output in published Canvas packages and corrects the transactional package entrypoint paths used by consumers.
26
-
27
- ## 0.0.2
28
-
29
- ### Patch Changes
30
-
31
- - bef5ec7: Ensure the admin server binary is executable in Docker images.
32
- - 2ee81b6: Fix rich text editor transaction sync issues and stabilize localized field test behavior.
@@ -1,107 +0,0 @@
1
- import * as React from "react";
2
- import {
3
- Html,
4
- Head,
5
- Body,
6
- Container,
7
- Section,
8
- Text,
9
- Button,
10
- Hr,
11
- } from "@react-email/components";
12
-
13
- interface ResetPasswordEmailProps {
14
- resetUrl: string;
15
- userName?: string;
16
- }
17
-
18
- export default function ResetPasswordEmail({
19
- resetUrl = "https://example.com/reset-password",
20
- userName = "there",
21
- }: ResetPasswordEmailProps) {
22
- return (
23
- <Html lang="en">
24
- <Head />
25
- <Body style={main}>
26
- <Container style={container}>
27
- <Section style={content}>
28
- <Text style={heading}>Reset your password</Text>
29
- <Text style={paragraph}>Hi {userName},</Text>
30
- <Text style={paragraph}>
31
- We received a request to reset your password. Click the button below to create a new
32
- password:
33
- </Text>
34
- <Button href={resetUrl} style={button}>
35
- Reset Password
36
- </Button>
37
- <Hr style={hr} />
38
- <Text style={paragraph}>
39
- This link will expire in 1 hour for security reasons.
40
- </Text>
41
- <Text style={footer}>
42
- If you didn't request a password reset, you can safely ignore this email. Your
43
- password will not be changed.
44
- </Text>
45
- </Section>
46
- </Container>
47
- </Body>
48
- </Html>
49
- );
50
- }
51
-
52
- const main = {
53
- backgroundColor: "#f6f9fc",
54
- fontFamily:
55
- '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
56
- };
57
-
58
- const container = {
59
- backgroundColor: "#ffffff",
60
- margin: "0 auto",
61
- padding: "20px 0 48px",
62
- marginBottom: "64px",
63
- };
64
-
65
- const content = {
66
- padding: "0 48px",
67
- };
68
-
69
- const heading = {
70
- fontSize: "32px",
71
- lineHeight: "1.3",
72
- fontWeight: "700",
73
- color: "#484848",
74
- padding: "17px 0 0",
75
- };
76
-
77
- const paragraph = {
78
- margin: "0 0 15px",
79
- fontSize: "15px",
80
- lineHeight: "1.4",
81
- color: "#3c4149",
82
- };
83
-
84
- const button = {
85
- backgroundColor: "#000000",
86
- borderRadius: "5px",
87
- color: "#fff",
88
- fontSize: "16px",
89
- fontWeight: "bold",
90
- textDecoration: "none",
91
- textAlign: "center" as const,
92
- display: "block",
93
- width: "100%",
94
- padding: "12px",
95
- margin: "20px 0",
96
- };
97
-
98
- const hr = {
99
- borderColor: "#dfe1e4",
100
- margin: "42px 0 26px",
101
- };
102
-
103
- const footer = {
104
- color: "#8898aa",
105
- fontSize: "12px",
106
- lineHeight: "16px",
107
- };
@@ -1,109 +0,0 @@
1
- import * as React from "react";
2
- import {
3
- Html,
4
- Head,
5
- Body,
6
- Container,
7
- Section,
8
- Text,
9
- Button,
10
- Hr,
11
- } from "@react-email/components";
12
-
13
- interface TeamInviteEmailProps {
14
- teamName: string;
15
- inviterName: string;
16
- inviteUrl: string;
17
- recipientEmail: string;
18
- }
19
-
20
- export default function TeamInviteEmail({
21
- teamName = "Your Team",
22
- inviterName = "A team member",
23
- inviteUrl = "https://example.com/accept-invite",
24
- recipientEmail = "user@example.com",
25
- }: TeamInviteEmailProps) {
26
- return (
27
- <Html lang="en">
28
- <Head />
29
- <Body style={main}>
30
- <Container style={container}>
31
- <Section style={content}>
32
- <Text style={heading}>You've been invited to join a team</Text>
33
- <Text style={paragraph}>
34
- {inviterName} has invited you to join <strong>{teamName}</strong>.
35
- </Text>
36
- <Text style={paragraph}>
37
- Click the button below to accept the invitation and join the team:
38
- </Text>
39
- <Button href={inviteUrl} style={button}>
40
- Accept Invitation
41
- </Button>
42
- <Hr style={hr} />
43
- <Text style={footer}>
44
- This invitation was sent to {recipientEmail}. If you weren't expecting this
45
- invitation, you can safely ignore this email.
46
- </Text>
47
- </Section>
48
- </Container>
49
- </Body>
50
- </Html>
51
- );
52
- }
53
-
54
- const main = {
55
- backgroundColor: "#f6f9fc",
56
- fontFamily:
57
- '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
58
- };
59
-
60
- const container = {
61
- backgroundColor: "#ffffff",
62
- margin: "0 auto",
63
- padding: "20px 0 48px",
64
- marginBottom: "64px",
65
- };
66
-
67
- const content = {
68
- padding: "0 48px",
69
- };
70
-
71
- const heading = {
72
- fontSize: "32px",
73
- lineHeight: "1.3",
74
- fontWeight: "700",
75
- color: "#484848",
76
- padding: "17px 0 0",
77
- };
78
-
79
- const paragraph = {
80
- margin: "0 0 15px",
81
- fontSize: "15px",
82
- lineHeight: "1.4",
83
- color: "#3c4149",
84
- };
85
-
86
- const button = {
87
- backgroundColor: "#000000",
88
- borderRadius: "5px",
89
- color: "#fff",
90
- fontSize: "16px",
91
- fontWeight: "bold",
92
- textDecoration: "none",
93
- textAlign: "center" as const,
94
- display: "block",
95
- width: "100%",
96
- padding: "12px",
97
- margin: "20px 0",
98
- };
99
-
100
- const hr = {
101
- borderColor: "#dfe1e4",
102
- margin: "42px 0 26px",
103
- };
104
-
105
- const footer = {
106
- color: "#8898aa",
107
- fontSize: "12px",
108
- lineHeight: "16px",
109
- };
package/src/client.ts DELETED
@@ -1,59 +0,0 @@
1
- import type { PlunkSendEmailRequest, PlunkSendEmailResponse } from "./types";
2
-
3
- const PLUNK_API_BASE_URL = "https://next-api.useplunk.com";
4
-
5
- export class PlunkClient {
6
- private apiKey: string;
7
-
8
- constructor(apiKey?: string) {
9
- this.apiKey = apiKey || process.env.PLUNK_API_KEY || "";
10
-
11
- if (!this.apiKey) {
12
- throw new Error(
13
- "Plunk API key is required. Set PLUNK_API_KEY environment variable or pass it to the constructor."
14
- );
15
- }
16
- }
17
-
18
- async sendEmail(request: PlunkSendEmailRequest): Promise<PlunkSendEmailResponse> {
19
- try {
20
- const response = await fetch(`${PLUNK_API_BASE_URL}/v1/send`, {
21
- method: "POST",
22
- headers: {
23
- "Authorization": `Bearer ${this.apiKey}`,
24
- "Content-Type": "application/json",
25
- },
26
- body: JSON.stringify(request),
27
- });
28
-
29
- const data = await response.json() as PlunkSendEmailResponse;
30
-
31
- if (!response.ok) {
32
- throw new Error(
33
- data.error?.message || `Failed to send email: ${response.statusText}`
34
- );
35
- }
36
-
37
- return data;
38
- } catch (error) {
39
- if (error instanceof Error) {
40
- throw error;
41
- }
42
- throw new Error("An unknown error occurred while sending email");
43
- }
44
- }
45
- }
46
-
47
- // Export a default instance
48
- let defaultClient: PlunkClient | null = null;
49
-
50
- export function getDefaultClient(): PlunkClient {
51
- if (!defaultClient) {
52
- defaultClient = new PlunkClient();
53
- }
54
- return defaultClient;
55
- }
56
-
57
- export function setDefaultClient(client: PlunkClient): void {
58
- defaultClient = client;
59
- }
package/src/index.ts DELETED
@@ -1,63 +0,0 @@
1
- import { getDefaultClient, PlunkClient, setDefaultClient } from "./client";
2
- import {
3
- renderTeamInvite,
4
- renderResetPassword,
5
- } from "./templates";
6
- import type {
7
- TeamInviteProps,
8
- ResetPasswordProps,
9
- SendEmailOptions,
10
- PlunkSendEmailResponse,
11
- } from "./types";
12
-
13
- /**
14
- * Send a team invitation email
15
- */
16
- export async function sendTeamInvite(
17
- to: string,
18
- props: TeamInviteProps,
19
- options?: SendEmailOptions
20
- ): Promise<PlunkSendEmailResponse> {
21
- const client = getDefaultClient();
22
- const html = await renderTeamInvite(props);
23
-
24
- return await client.sendEmail({
25
- to,
26
- subject: `Join ${props.teamName}`,
27
- body: html,
28
- from: options?.from,
29
- reply: options?.replyTo,
30
- headers: options?.headers,
31
- });
32
- }
33
-
34
- /**
35
- * Send a password reset email
36
- */
37
- export async function sendResetPassword(
38
- to: string,
39
- props: ResetPasswordProps,
40
- options?: SendEmailOptions
41
- ): Promise<PlunkSendEmailResponse> {
42
- const client = getDefaultClient();
43
- const html = await renderResetPassword(props);
44
-
45
- return await client.sendEmail({
46
- to,
47
- subject: "Reset your password",
48
- body: html,
49
- from: options?.from,
50
- reply: options?.replyTo,
51
- headers: options?.headers,
52
- });
53
- }
54
-
55
- // Export types and utilities
56
- export type {
57
- TeamInviteProps,
58
- ResetPasswordProps,
59
- SendEmailOptions,
60
- PlunkSendEmailResponse,
61
- };
62
-
63
- export { PlunkClient, setDefaultClient };
package/src/templates.tsx DELETED
@@ -1,15 +0,0 @@
1
- import { render } from "@react-email/components";
2
- import TeamInviteEmail from "../emails/team-invite";
3
- import ResetPasswordEmail from "../emails/reset-password";
4
- import type {
5
- TeamInviteProps,
6
- ResetPasswordProps,
7
- } from "./types";
8
-
9
- export async function renderTeamInvite(props: TeamInviteProps): Promise<string> {
10
- return await render(<TeamInviteEmail {...props} />);
11
- }
12
-
13
- export async function renderResetPassword(props: ResetPasswordProps): Promise<string> {
14
- return await render(<ResetPasswordEmail {...props} />);
15
- }
package/src/types.ts DELETED
@@ -1,54 +0,0 @@
1
- // Plunk API types
2
- export interface PlunkSendEmailRequest {
3
- to: string | { name: string; email: string } | Array<string | { name: string; email: string }>;
4
- subject: string;
5
- body: string;
6
- from?: string | { name: string; email: string };
7
- name?: string;
8
- subscribed?: boolean;
9
- data?: Record<string, unknown>;
10
- headers?: Record<string, string>;
11
- reply?: string;
12
- attachments?: Array<{
13
- filename: string;
14
- content: string; // base64
15
- contentType: string;
16
- }>;
17
- }
18
-
19
- export interface PlunkSendEmailResponse {
20
- success: boolean;
21
- data?: {
22
- emails: Array<{
23
- contact: string;
24
- email: string;
25
- }>;
26
- timestamp: string;
27
- };
28
- error?: {
29
- code: string;
30
- error: string;
31
- message: string;
32
- timestamp: string;
33
- };
34
- }
35
-
36
- // Email template props
37
- export interface TeamInviteProps {
38
- teamName: string;
39
- inviterName: string;
40
- inviteUrl: string;
41
- recipientEmail: string;
42
- }
43
-
44
- export interface ResetPasswordProps {
45
- resetUrl: string;
46
- userName?: string;
47
- }
48
-
49
- // Send email options
50
- export interface SendEmailOptions {
51
- from?: string | { name: string; email: string };
52
- replyTo?: string;
53
- headers?: Record<string, string>;
54
- }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "@cms0/typescript-config/base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "jsx": "react-jsx"
6
- },
7
- "include": [
8
- "src",
9
- "emails"
10
- ]
11
- }