better-auth-cloudflare-email 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Stenhouse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # better-auth-cloudflare-email-plugin
2
+
3
+ Send emails through [Cloudflare Email Service](https://developers.cloudflare.com/email-service/) from [Better Auth](https://better-auth.com) — with zero configuration for each callback.
4
+
5
+ Two transports, one interface:
6
+
7
+ - **`cloudflareEmail.workers()`** — inside a Cloudflare Worker, uses the `send_email` binding directly (no API key, zero network hop)
8
+ - **`cloudflareEmail.api()`** — from any runtime (Node.js, Bun, Deno, Vercel, etc.), uses the Cloudflare REST API
9
+
10
+ Both wire into all 6 Better Auth email callbacks automatically and ship with clean, responsive HTML templates out of the box.
11
+
12
+ ## Callbacks handled
13
+
14
+ | Callback | Source | Triggered by |
15
+ |---|---|---|
16
+ | `sendVerificationEmail` | Core config | Sign-up, manual verify |
17
+ | `sendResetPassword` | Core config | Password reset request |
18
+ | `sendChangeEmailConfirmation` | Core config | Email change request |
19
+ | `sendDeleteAccountVerification` | Core config | Account deletion request |
20
+ | `sendMagicLink` | `magicLink()` plugin | Magic link sign-in |
21
+ | `sendVerificationOTP` | `emailOTP()` plugin | OTP sign-in / verify / reset |
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install better-auth-cloudflare-email
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ### Cloudflare Workers (binding transport)
32
+
33
+ ```ts
34
+ import { betterAuth } from "better-auth";
35
+ import { magicLink, emailOTP } from "better-auth/plugins";
36
+ import { cloudflareEmail } from "better-auth-cloudflare-email";
37
+
38
+ function createAuth(env: Env) {
39
+ const email = cloudflareEmail.workers({
40
+ binding: env.EMAIL,
41
+ from: "MyApp <noreply@myapp.com>",
42
+ appName: "MyApp",
43
+ });
44
+
45
+ return betterAuth({
46
+ ...email.config,
47
+ plugins: [
48
+ magicLink({ sendMagicLink: email.sendMagicLink }),
49
+ emailOTP({ sendVerificationOTP: email.sendVerificationOTP }),
50
+ ],
51
+ });
52
+ }
53
+ ```
54
+
55
+ Add the binding to your `wrangler.jsonc`:
56
+
57
+ ```jsonc
58
+ {
59
+ "send_email": [{ "name": "EMAIL" }]
60
+ }
61
+ ```
62
+
63
+ ### Any runtime (REST API transport)
64
+
65
+ ```ts
66
+ import { betterAuth } from "better-auth";
67
+ import { magicLink, emailOTP } from "better-auth/plugins";
68
+ import { cloudflareEmail } from "better-auth-cloudflare-email";
69
+
70
+ const email = cloudflareEmail.api({
71
+ accountId: process.env.CF_ACCOUNT_ID!,
72
+ apiToken: process.env.CF_API_TOKEN!,
73
+ from: "MyApp <noreply@myapp.com>",
74
+ appName: "MyApp",
75
+ });
76
+
77
+ export const auth = betterAuth({
78
+ ...email.config,
79
+ plugins: [
80
+ magicLink({ sendMagicLink: email.sendMagicLink }),
81
+ emailOTP({ sendVerificationOTP: email.sendVerificationOTP }),
82
+ ],
83
+ });
84
+ ```
85
+
86
+ ## How it works
87
+
88
+ `cloudflareEmail.workers()` and `cloudflareEmail.api()` both return the same object:
89
+
90
+ ```ts
91
+ {
92
+ // Spread into betterAuth() — wires verification, reset, change email, delete account
93
+ config: { emailVerification, emailAndPassword, user },
94
+
95
+ // Wire into plugins manually
96
+ sendMagicLink,
97
+ sendVerificationOTP,
98
+
99
+ // Individual callbacks (if you prefer manual wiring)
100
+ sendVerificationEmail,
101
+ sendResetPassword,
102
+ sendChangeEmailConfirmation,
103
+ sendDeleteAccountVerification,
104
+
105
+ // Send any arbitrary email
106
+ sendRaw(message: EmailMessage): Promise<EmailSendResult>,
107
+ }
108
+ ```
109
+
110
+ The `config` object is designed to be spread into your `betterAuth()` call. Override any defaults after spreading:
111
+
112
+ ```ts
113
+ const email = cloudflareEmail.workers({ binding: env.EMAIL, from: "..." });
114
+
115
+ export const auth = betterAuth({
116
+ ...email.config,
117
+ emailAndPassword: {
118
+ ...email.config.emailAndPassword,
119
+ requireEmailVerification: true, // your override
120
+ minPasswordLength: 12, // your override
121
+ },
122
+ });
123
+ ```
124
+
125
+ ## Hono + Workers example
126
+
127
+ ```ts
128
+ import { Hono } from "hono";
129
+ import { betterAuth } from "better-auth";
130
+ import { cloudflareEmail } from "better-auth-cloudflare-email";
131
+
132
+ interface Env {
133
+ EMAIL: import("./auth/cloudflare-email").EmailBinding;
134
+ DB: D1Database;
135
+ }
136
+
137
+ const app = new Hono<{ Bindings: Env }>();
138
+
139
+ app.on(["POST", "GET"], "/api/auth/*", (c) => {
140
+ const email = cloudflareEmail.workers({
141
+ binding: c.env.EMAIL,
142
+ from: "MyApp <noreply@myapp.com>",
143
+ });
144
+
145
+ const auth = betterAuth({
146
+ ...email.config,
147
+ // database, plugins, etc.
148
+ });
149
+
150
+ return auth.handler(c.req.raw);
151
+ });
152
+
153
+ export default app;
154
+ ```
155
+
156
+ ### Singleton auth with dynamic binding
157
+
158
+ If you don't want to create auth per-request, pass a function that resolves the binding at call time:
159
+
160
+ ```ts
161
+ import { getRequestContext } from "@cloudflare/next-on-pages";
162
+ import { cloudflareEmail } from "better-auth-cloudflare-email";
163
+
164
+ const email = cloudflareEmail.workers({
165
+ binding: () => getRequestContext().env.EMAIL,
166
+ from: "MyApp <noreply@myapp.com>",
167
+ });
168
+
169
+ export const auth = betterAuth({ ...email.config });
170
+ ```
171
+
172
+ ## Custom templates
173
+
174
+ Override any template by passing a function that returns `{ subject, html, text }`:
175
+
176
+ ```ts
177
+ const email = cloudflareEmail.workers({
178
+ binding: env.EMAIL,
179
+ from: "MyApp <noreply@myapp.com>",
180
+ appName: "MyApp",
181
+ templates: {
182
+ verifyEmail: ({ appName, url, userName }) => ({
183
+ subject: `Welcome to ${appName}!`,
184
+ html: `<h1>Hey ${userName}!</h1><a href="${url}">Verify your email</a>`,
185
+ text: `Verify your email: ${url}`,
186
+ }),
187
+ // resetPassword, changeEmail, deleteAccount, magicLink, otp
188
+ },
189
+ });
190
+ ```
191
+
192
+ ### Template data available
193
+
194
+ | Field | Type | Available in |
195
+ |---|---|---|
196
+ | `appName` | `string` | All templates |
197
+ | `url` | `string` | All except `otp` |
198
+ | `token` | `string` | All except `otp` |
199
+ | `userName` | `string \| undefined` | `verifyEmail`, `resetPassword` |
200
+ | `otp` | `string` | `otp` only |
201
+ | `email` | `string` | `magicLink`, `otp` |
202
+
203
+ ## Sending arbitrary emails
204
+
205
+ Use `sendRaw` to send any email outside of Better Auth's callbacks:
206
+
207
+ ```ts
208
+ await email.sendRaw({
209
+ to: "user@example.com",
210
+ from: "support@myapp.com",
211
+ subject: "Your invoice",
212
+ html: "<p>Thanks for your purchase.</p>",
213
+ text: "Thanks for your purchase.",
214
+ attachments: [{
215
+ filename: "invoice.pdf",
216
+ content: base64String,
217
+ contentType: "application/pdf",
218
+ disposition: "attachment",
219
+ }],
220
+ });
221
+ ```
222
+
223
+ ## Transport comparison
224
+
225
+ | | `cloudflareEmail.workers()` | `cloudflareEmail.api()` |
226
+ |---|---|---|
227
+ | Runtime | Cloudflare Workers only | Any (Node, Bun, Deno, Vercel...) |
228
+ | Auth | `send_email` binding (no key) | `CF_ACCOUNT_ID` + `CF_API_TOKEN` |
229
+ | Latency | In-process, zero hop | HTTP round-trip to Cloudflare API |
230
+ | Config | `binding: env.EMAIL` | `accountId` + `apiToken` |
231
+ | Output | Identical | Identical |
232
+
233
+ ## Cloudflare Email Service setup
234
+
235
+ 1. Your domain must use [Cloudflare DNS](https://developers.cloudflare.com/dns/)
236
+ 2. Go to **Cloudflare Dashboard > Compute & AI > Email Service > Email Sending**
237
+ 3. Select your domain and add the required DNS records (SPF, DKIM)
238
+ 4. For the REST API transport, create an [API token](https://dash.cloudflare.com/profile/api-tokens) with email send permissions
239
+
240
+ > Cloudflare Email Service is currently in **private beta**. It requires a Workers Paid plan.
241
+
242
+ ## License
243
+
244
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ cloudflareEmail: () => cloudflareEmail
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/auth/cloudflare-email.ts
28
+ function layout(title, body, appName) {
29
+ return `<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
32
+ <title>${title}</title>
33
+ <style>
34
+ body{margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
35
+ .wrap{max-width:480px;margin:40px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08)}
36
+ .header{background:#18181b;padding:24px 32px;color:#fff;font-size:18px;font-weight:600}
37
+ .body{padding:32px}
38
+ .body p{margin:0 0 16px;color:#3f3f46;line-height:1.6;font-size:15px}
39
+ .btn{display:inline-block;padding:12px 28px;background:#18181b;color:#fff!important;text-decoration:none;border-radius:8px;font-weight:500;font-size:15px}
40
+ .code{display:inline-block;padding:12px 24px;background:#f4f4f5;border-radius:8px;font-size:28px;font-weight:700;letter-spacing:6px;color:#18181b}
41
+ .footer{padding:16px 32px;font-size:12px;color:#a1a1aa;border-top:1px solid #f4f4f5}
42
+ </style></head>
43
+ <body><div class="wrap">
44
+ <div class="header">${appName}</div>
45
+ <div class="body">${body}</div>
46
+ <div class="footer">You received this email because an action was requested on your account. If you didn't request this, you can safely ignore it.</div>
47
+ </div></body></html>`;
48
+ }
49
+ var defaultTemplates = {
50
+ verifyEmail: ({ appName, url, userName }) => ({
51
+ subject: `Verify your email \u2013 ${appName}`,
52
+ html: layout(
53
+ "Verify Email",
54
+ `<p>Hi${userName ? ` ${userName}` : ""},</p>
55
+ <p>Thanks for signing up. Please verify your email address by clicking the button below.</p>
56
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Verify Email</a></p>
57
+ <p style="font-size:13px;color:#71717a">If the button doesn't work, copy and paste this link into your browser:<br>${url}</p>`,
58
+ appName
59
+ ),
60
+ text: `Verify your email for ${appName}: ${url}`
61
+ }),
62
+ resetPassword: ({ appName, url, userName }) => ({
63
+ subject: `Reset your password \u2013 ${appName}`,
64
+ html: layout(
65
+ "Reset Password",
66
+ `<p>Hi${userName ? ` ${userName}` : ""},</p>
67
+ <p>We received a request to reset your password. Click the button below to choose a new one.</p>
68
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Reset Password</a></p>
69
+ <p style="font-size:13px;color:#71717a">This link expires in 1 hour. If you didn't request a password reset, you can ignore this email.</p>`,
70
+ appName
71
+ ),
72
+ text: `Reset your password for ${appName}: ${url}`
73
+ }),
74
+ changeEmail: ({ appName, url }) => ({
75
+ subject: `Confirm email change \u2013 ${appName}`,
76
+ html: layout(
77
+ "Confirm Email Change",
78
+ `<p>You requested to change the email address on your ${appName} account.</p>
79
+ <p>Click the button below to confirm this change.</p>
80
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Confirm Change</a></p>
81
+ <p style="font-size:13px;color:#71717a">If you didn't request this, please secure your account immediately.</p>`,
82
+ appName
83
+ ),
84
+ text: `Confirm email change for ${appName}: ${url}`
85
+ }),
86
+ deleteAccount: ({ appName, url }) => ({
87
+ subject: `Confirm account deletion \u2013 ${appName}`,
88
+ html: layout(
89
+ "Delete Account",
90
+ `<p>You requested to delete your ${appName} account. This action is permanent.</p>
91
+ <p>Click the button below to confirm deletion.</p>
92
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn" style="background:#dc2626">Delete Account</a></p>
93
+ <p style="font-size:13px;color:#71717a">If you didn't request this, please ignore this email and secure your account.</p>`,
94
+ appName
95
+ ),
96
+ text: `Confirm account deletion for ${appName}: ${url}`
97
+ }),
98
+ magicLink: ({ appName, url }) => ({
99
+ subject: `Sign in to ${appName}`,
100
+ html: layout(
101
+ "Magic Link",
102
+ `<p>Click the button below to sign in to your ${appName} account. This link expires in 15 minutes.</p>
103
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Sign In</a></p>
104
+ <p style="font-size:13px;color:#71717a">If you didn't request this, you can safely ignore this email.</p>`,
105
+ appName
106
+ ),
107
+ text: `Sign in to ${appName}: ${url}`
108
+ }),
109
+ otp: ({ appName, otp }) => ({
110
+ subject: `Your verification code \u2013 ${appName}`,
111
+ html: layout(
112
+ "Verification Code",
113
+ `<p>Use the following code to continue. It expires in 5 minutes.</p>
114
+ <p style="text-align:center;margin:24px 0"><span class="code">${otp}</span></p>
115
+ <p style="font-size:13px;color:#71717a">If you didn't request this code, you can safely ignore this email.</p>`,
116
+ appName
117
+ ),
118
+ text: `Your ${appName} verification code: ${otp}`
119
+ })
120
+ };
121
+ function createWorkersTransport(opts) {
122
+ return {
123
+ async send(message) {
124
+ const binding = typeof opts.binding === "function" ? opts.binding() : opts.binding;
125
+ return binding.send(message);
126
+ }
127
+ };
128
+ }
129
+ function createApiTransport(opts) {
130
+ const baseUrl = opts.baseUrl ?? "https://api.cloudflare.com/client/v4";
131
+ const url = `${baseUrl}/accounts/${opts.accountId}/email-service/send`;
132
+ return {
133
+ async send(message) {
134
+ const res = await fetch(url, {
135
+ method: "POST",
136
+ headers: {
137
+ Authorization: `Bearer ${opts.apiToken}`,
138
+ "Content-Type": "application/json"
139
+ },
140
+ body: JSON.stringify(message)
141
+ });
142
+ if (!res.ok) {
143
+ const body = await res.text();
144
+ throw new Error(`Cloudflare Email API error (${res.status}): ${body}`);
145
+ }
146
+ const json = await res.json();
147
+ return { messageId: json.result?.messageId ?? "" };
148
+ }
149
+ };
150
+ }
151
+ function build(transport, shared) {
152
+ const appName = shared.appName ?? "Our App";
153
+ const from = shared.from;
154
+ const t = { ...defaultTemplates, ...shared.templates };
155
+ function fire(to, rendered) {
156
+ void transport.send({ to, from, subject: rendered.subject, html: rendered.html, text: rendered.text });
157
+ }
158
+ const sendVerificationEmail = async (data, _request) => {
159
+ fire(data.user.email, t.verifyEmail({ appName, url: data.url, token: data.token, userName: data.user.name }));
160
+ };
161
+ const sendResetPassword = async (data, _request) => {
162
+ fire(data.user.email, t.resetPassword({ appName, url: data.url, token: data.token, userName: data.user.name }));
163
+ };
164
+ const sendChangeEmailConfirmation = async (data, _request) => {
165
+ fire(data.newEmail, t.changeEmail({ appName, url: data.url, token: data.token }));
166
+ };
167
+ const sendDeleteAccountVerification = async (data, _request) => {
168
+ fire(data.user.email, t.deleteAccount({ appName, url: data.url, token: data.token }));
169
+ };
170
+ const sendMagicLink = async (data) => {
171
+ fire(data.email, t.magicLink({ appName, url: data.url, token: data.token, email: data.email }));
172
+ };
173
+ const sendVerificationOTP = async (data) => {
174
+ fire(data.email, t.otp({ appName, otp: data.otp, email: data.email }));
175
+ };
176
+ return {
177
+ config: {
178
+ emailVerification: { sendVerificationEmail, sendOnSignUp: true },
179
+ emailAndPassword: { enabled: true, sendResetPassword },
180
+ user: {
181
+ changeEmail: { enabled: true, sendChangeEmailConfirmation },
182
+ deleteUser: { enabled: true, sendDeleteAccountVerification }
183
+ }
184
+ },
185
+ sendVerificationEmail,
186
+ sendResetPassword,
187
+ sendChangeEmailConfirmation,
188
+ sendDeleteAccountVerification,
189
+ sendMagicLink,
190
+ sendVerificationOTP,
191
+ sendRaw: (message) => transport.send(message)
192
+ };
193
+ }
194
+ var cloudflareEmail = {
195
+ /**
196
+ * Use inside a Cloudflare Worker — sends via the `send_email` binding.
197
+ * Zero network hop, no API key needed.
198
+ *
199
+ * ```ts
200
+ * const email = cloudflareEmail.workers({
201
+ * binding: env.EMAIL,
202
+ * from: "App <noreply@app.com>",
203
+ * });
204
+ * ```
205
+ */
206
+ workers(opts) {
207
+ return build(createWorkersTransport(opts), opts);
208
+ },
209
+ /**
210
+ * Use from any runtime (Node.js, Bun, Deno, Vercel, etc.) — sends via
211
+ * the Cloudflare Email Service REST API.
212
+ *
213
+ * ```ts
214
+ * const email = cloudflareEmail.api({
215
+ * accountId: process.env.CF_ACCOUNT_ID,
216
+ * apiToken: process.env.CF_API_TOKEN,
217
+ * from: "App <noreply@app.com>",
218
+ * });
219
+ * ```
220
+ */
221
+ api(opts) {
222
+ return build(createApiTransport(opts), opts);
223
+ }
224
+ };
225
+ // Annotate the CommonJS export names for ESM import in node:
226
+ 0 && (module.exports = {
227
+ cloudflareEmail
228
+ });
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Better Auth + Cloudflare Email Service integration.
3
+ *
4
+ * Two transports, one interface:
5
+ *
6
+ * // Inside a Cloudflare Worker — uses the send_email binding (zero latency, no API key)
7
+ * import { cloudflareEmail } from "./cloudflare-email";
8
+ * const email = cloudflareEmail.workers({ binding: env.EMAIL, from: "..." });
9
+ *
10
+ * // Anywhere else — uses the Cloudflare REST API
11
+ * import { cloudflareEmail } from "./cloudflare-email";
12
+ * const email = cloudflareEmail.api({ accountId: "...", apiToken: "...", from: "..." });
13
+ *
14
+ * Both return the same object shape — spread `email.config` into betterAuth()
15
+ * and wire plugin callbacks identically.
16
+ */
17
+ interface EmailMessage {
18
+ to: string | string[];
19
+ from: string;
20
+ subject: string;
21
+ html?: string;
22
+ text?: string;
23
+ cc?: string | string[];
24
+ bcc?: string | string[];
25
+ replyTo?: string;
26
+ headers?: Record<string, string>;
27
+ attachments?: EmailAttachment[];
28
+ }
29
+ interface EmailAttachment {
30
+ filename: string;
31
+ content: string;
32
+ contentType: string;
33
+ disposition: "attachment" | "inline";
34
+ contentId?: string;
35
+ }
36
+ interface EmailSendResult {
37
+ messageId: string;
38
+ }
39
+ interface EmailTransport {
40
+ send(message: EmailMessage): Promise<EmailSendResult>;
41
+ }
42
+ interface TemplateData {
43
+ appName: string;
44
+ url?: string;
45
+ token?: string;
46
+ otp?: string;
47
+ email?: string;
48
+ userName?: string;
49
+ }
50
+ type TemplateFn = (data: TemplateData) => {
51
+ subject: string;
52
+ html: string;
53
+ text: string;
54
+ };
55
+ interface Templates {
56
+ verifyEmail?: TemplateFn;
57
+ resetPassword?: TemplateFn;
58
+ changeEmail?: TemplateFn;
59
+ deleteAccount?: TemplateFn;
60
+ magicLink?: TemplateFn;
61
+ otp?: TemplateFn;
62
+ }
63
+ interface SharedOptions {
64
+ /** Default "from" address, e.g. "MyApp <noreply@myapp.com>". */
65
+ from: string;
66
+ /** Application name shown in email templates. Default: "Our App". */
67
+ appName?: string;
68
+ /** Override default email templates per type. */
69
+ templates?: Templates;
70
+ }
71
+ /** Cloudflare Workers send_email binding. */
72
+ interface EmailBinding {
73
+ send(message: EmailMessage): Promise<EmailSendResult>;
74
+ sendBatch?(messages: EmailMessage[]): Promise<{
75
+ results: Array<{
76
+ success: boolean;
77
+ messageId?: string;
78
+ error?: string;
79
+ }>;
80
+ }>;
81
+ }
82
+ interface WorkersOptions extends SharedOptions {
83
+ /** The send_email binding, or a function that returns it (for singleton auth). */
84
+ binding: EmailBinding | (() => EmailBinding);
85
+ }
86
+ interface ApiOptions extends SharedOptions {
87
+ /** Cloudflare Account ID. */
88
+ accountId: string;
89
+ /** Cloudflare API Token with email send permissions. */
90
+ apiToken: string;
91
+ /** Override the base URL. Default: "https://api.cloudflare.com/client/v4" */
92
+ baseUrl?: string;
93
+ }
94
+ interface CloudflareEmailResult {
95
+ /** Spread into betterAuth() to wire up core email callbacks automatically. */
96
+ config: {
97
+ emailVerification: {
98
+ sendVerificationEmail: (data: {
99
+ user: {
100
+ email: string;
101
+ name?: string;
102
+ };
103
+ url: string;
104
+ token: string;
105
+ }, request?: Request) => Promise<void>;
106
+ sendOnSignUp: true;
107
+ };
108
+ emailAndPassword: {
109
+ enabled: true;
110
+ sendResetPassword: (data: {
111
+ user: {
112
+ email: string;
113
+ name?: string;
114
+ };
115
+ url: string;
116
+ token: string;
117
+ }, request?: Request) => Promise<void>;
118
+ };
119
+ user: {
120
+ changeEmail: {
121
+ enabled: true;
122
+ sendChangeEmailConfirmation: (data: {
123
+ user: {
124
+ email: string;
125
+ name?: string;
126
+ };
127
+ newEmail: string;
128
+ url: string;
129
+ token: string;
130
+ }, request?: Request) => Promise<void>;
131
+ };
132
+ deleteUser: {
133
+ enabled: true;
134
+ sendDeleteAccountVerification: (data: {
135
+ user: {
136
+ email: string;
137
+ name?: string;
138
+ };
139
+ url: string;
140
+ token: string;
141
+ }, request?: Request) => Promise<void>;
142
+ };
143
+ };
144
+ };
145
+ /** For magicLink() plugin. */
146
+ sendMagicLink: (data: {
147
+ email: string;
148
+ url: string;
149
+ token: string;
150
+ metadata?: Record<string, unknown>;
151
+ }) => Promise<void>;
152
+ /** For emailOTP() plugin. */
153
+ sendVerificationOTP: (data: {
154
+ email: string;
155
+ otp: string;
156
+ type: string;
157
+ }) => Promise<void>;
158
+ /** Individual callbacks if you prefer manual wiring. */
159
+ sendVerificationEmail: (data: {
160
+ user: {
161
+ email: string;
162
+ name?: string;
163
+ };
164
+ url: string;
165
+ token: string;
166
+ }, request?: Request) => Promise<void>;
167
+ sendResetPassword: (data: {
168
+ user: {
169
+ email: string;
170
+ name?: string;
171
+ };
172
+ url: string;
173
+ token: string;
174
+ }, request?: Request) => Promise<void>;
175
+ sendChangeEmailConfirmation: (data: {
176
+ user: {
177
+ email: string;
178
+ name?: string;
179
+ };
180
+ newEmail: string;
181
+ url: string;
182
+ token: string;
183
+ }, request?: Request) => Promise<void>;
184
+ sendDeleteAccountVerification: (data: {
185
+ user: {
186
+ email: string;
187
+ name?: string;
188
+ };
189
+ url: string;
190
+ token: string;
191
+ }, request?: Request) => Promise<void>;
192
+ /** Send an arbitrary email through the underlying transport. */
193
+ sendRaw: (message: EmailMessage) => Promise<EmailSendResult>;
194
+ }
195
+ declare const cloudflareEmail: {
196
+ /**
197
+ * Use inside a Cloudflare Worker — sends via the `send_email` binding.
198
+ * Zero network hop, no API key needed.
199
+ *
200
+ * ```ts
201
+ * const email = cloudflareEmail.workers({
202
+ * binding: env.EMAIL,
203
+ * from: "App <noreply@app.com>",
204
+ * });
205
+ * ```
206
+ */
207
+ workers(opts: WorkersOptions): CloudflareEmailResult;
208
+ /**
209
+ * Use from any runtime (Node.js, Bun, Deno, Vercel, etc.) — sends via
210
+ * the Cloudflare Email Service REST API.
211
+ *
212
+ * ```ts
213
+ * const email = cloudflareEmail.api({
214
+ * accountId: process.env.CF_ACCOUNT_ID,
215
+ * apiToken: process.env.CF_API_TOKEN,
216
+ * from: "App <noreply@app.com>",
217
+ * });
218
+ * ```
219
+ */
220
+ api(opts: ApiOptions): CloudflareEmailResult;
221
+ };
222
+
223
+ export { type ApiOptions, type EmailAttachment, type EmailBinding, type EmailMessage, type EmailSendResult, type EmailTransport, type TemplateData, type TemplateFn, type Templates, type WorkersOptions, cloudflareEmail };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Better Auth + Cloudflare Email Service integration.
3
+ *
4
+ * Two transports, one interface:
5
+ *
6
+ * // Inside a Cloudflare Worker — uses the send_email binding (zero latency, no API key)
7
+ * import { cloudflareEmail } from "./cloudflare-email";
8
+ * const email = cloudflareEmail.workers({ binding: env.EMAIL, from: "..." });
9
+ *
10
+ * // Anywhere else — uses the Cloudflare REST API
11
+ * import { cloudflareEmail } from "./cloudflare-email";
12
+ * const email = cloudflareEmail.api({ accountId: "...", apiToken: "...", from: "..." });
13
+ *
14
+ * Both return the same object shape — spread `email.config` into betterAuth()
15
+ * and wire plugin callbacks identically.
16
+ */
17
+ interface EmailMessage {
18
+ to: string | string[];
19
+ from: string;
20
+ subject: string;
21
+ html?: string;
22
+ text?: string;
23
+ cc?: string | string[];
24
+ bcc?: string | string[];
25
+ replyTo?: string;
26
+ headers?: Record<string, string>;
27
+ attachments?: EmailAttachment[];
28
+ }
29
+ interface EmailAttachment {
30
+ filename: string;
31
+ content: string;
32
+ contentType: string;
33
+ disposition: "attachment" | "inline";
34
+ contentId?: string;
35
+ }
36
+ interface EmailSendResult {
37
+ messageId: string;
38
+ }
39
+ interface EmailTransport {
40
+ send(message: EmailMessage): Promise<EmailSendResult>;
41
+ }
42
+ interface TemplateData {
43
+ appName: string;
44
+ url?: string;
45
+ token?: string;
46
+ otp?: string;
47
+ email?: string;
48
+ userName?: string;
49
+ }
50
+ type TemplateFn = (data: TemplateData) => {
51
+ subject: string;
52
+ html: string;
53
+ text: string;
54
+ };
55
+ interface Templates {
56
+ verifyEmail?: TemplateFn;
57
+ resetPassword?: TemplateFn;
58
+ changeEmail?: TemplateFn;
59
+ deleteAccount?: TemplateFn;
60
+ magicLink?: TemplateFn;
61
+ otp?: TemplateFn;
62
+ }
63
+ interface SharedOptions {
64
+ /** Default "from" address, e.g. "MyApp <noreply@myapp.com>". */
65
+ from: string;
66
+ /** Application name shown in email templates. Default: "Our App". */
67
+ appName?: string;
68
+ /** Override default email templates per type. */
69
+ templates?: Templates;
70
+ }
71
+ /** Cloudflare Workers send_email binding. */
72
+ interface EmailBinding {
73
+ send(message: EmailMessage): Promise<EmailSendResult>;
74
+ sendBatch?(messages: EmailMessage[]): Promise<{
75
+ results: Array<{
76
+ success: boolean;
77
+ messageId?: string;
78
+ error?: string;
79
+ }>;
80
+ }>;
81
+ }
82
+ interface WorkersOptions extends SharedOptions {
83
+ /** The send_email binding, or a function that returns it (for singleton auth). */
84
+ binding: EmailBinding | (() => EmailBinding);
85
+ }
86
+ interface ApiOptions extends SharedOptions {
87
+ /** Cloudflare Account ID. */
88
+ accountId: string;
89
+ /** Cloudflare API Token with email send permissions. */
90
+ apiToken: string;
91
+ /** Override the base URL. Default: "https://api.cloudflare.com/client/v4" */
92
+ baseUrl?: string;
93
+ }
94
+ interface CloudflareEmailResult {
95
+ /** Spread into betterAuth() to wire up core email callbacks automatically. */
96
+ config: {
97
+ emailVerification: {
98
+ sendVerificationEmail: (data: {
99
+ user: {
100
+ email: string;
101
+ name?: string;
102
+ };
103
+ url: string;
104
+ token: string;
105
+ }, request?: Request) => Promise<void>;
106
+ sendOnSignUp: true;
107
+ };
108
+ emailAndPassword: {
109
+ enabled: true;
110
+ sendResetPassword: (data: {
111
+ user: {
112
+ email: string;
113
+ name?: string;
114
+ };
115
+ url: string;
116
+ token: string;
117
+ }, request?: Request) => Promise<void>;
118
+ };
119
+ user: {
120
+ changeEmail: {
121
+ enabled: true;
122
+ sendChangeEmailConfirmation: (data: {
123
+ user: {
124
+ email: string;
125
+ name?: string;
126
+ };
127
+ newEmail: string;
128
+ url: string;
129
+ token: string;
130
+ }, request?: Request) => Promise<void>;
131
+ };
132
+ deleteUser: {
133
+ enabled: true;
134
+ sendDeleteAccountVerification: (data: {
135
+ user: {
136
+ email: string;
137
+ name?: string;
138
+ };
139
+ url: string;
140
+ token: string;
141
+ }, request?: Request) => Promise<void>;
142
+ };
143
+ };
144
+ };
145
+ /** For magicLink() plugin. */
146
+ sendMagicLink: (data: {
147
+ email: string;
148
+ url: string;
149
+ token: string;
150
+ metadata?: Record<string, unknown>;
151
+ }) => Promise<void>;
152
+ /** For emailOTP() plugin. */
153
+ sendVerificationOTP: (data: {
154
+ email: string;
155
+ otp: string;
156
+ type: string;
157
+ }) => Promise<void>;
158
+ /** Individual callbacks if you prefer manual wiring. */
159
+ sendVerificationEmail: (data: {
160
+ user: {
161
+ email: string;
162
+ name?: string;
163
+ };
164
+ url: string;
165
+ token: string;
166
+ }, request?: Request) => Promise<void>;
167
+ sendResetPassword: (data: {
168
+ user: {
169
+ email: string;
170
+ name?: string;
171
+ };
172
+ url: string;
173
+ token: string;
174
+ }, request?: Request) => Promise<void>;
175
+ sendChangeEmailConfirmation: (data: {
176
+ user: {
177
+ email: string;
178
+ name?: string;
179
+ };
180
+ newEmail: string;
181
+ url: string;
182
+ token: string;
183
+ }, request?: Request) => Promise<void>;
184
+ sendDeleteAccountVerification: (data: {
185
+ user: {
186
+ email: string;
187
+ name?: string;
188
+ };
189
+ url: string;
190
+ token: string;
191
+ }, request?: Request) => Promise<void>;
192
+ /** Send an arbitrary email through the underlying transport. */
193
+ sendRaw: (message: EmailMessage) => Promise<EmailSendResult>;
194
+ }
195
+ declare const cloudflareEmail: {
196
+ /**
197
+ * Use inside a Cloudflare Worker — sends via the `send_email` binding.
198
+ * Zero network hop, no API key needed.
199
+ *
200
+ * ```ts
201
+ * const email = cloudflareEmail.workers({
202
+ * binding: env.EMAIL,
203
+ * from: "App <noreply@app.com>",
204
+ * });
205
+ * ```
206
+ */
207
+ workers(opts: WorkersOptions): CloudflareEmailResult;
208
+ /**
209
+ * Use from any runtime (Node.js, Bun, Deno, Vercel, etc.) — sends via
210
+ * the Cloudflare Email Service REST API.
211
+ *
212
+ * ```ts
213
+ * const email = cloudflareEmail.api({
214
+ * accountId: process.env.CF_ACCOUNT_ID,
215
+ * apiToken: process.env.CF_API_TOKEN,
216
+ * from: "App <noreply@app.com>",
217
+ * });
218
+ * ```
219
+ */
220
+ api(opts: ApiOptions): CloudflareEmailResult;
221
+ };
222
+
223
+ export { type ApiOptions, type EmailAttachment, type EmailBinding, type EmailMessage, type EmailSendResult, type EmailTransport, type TemplateData, type TemplateFn, type Templates, type WorkersOptions, cloudflareEmail };
package/dist/index.js ADDED
@@ -0,0 +1,201 @@
1
+ // src/auth/cloudflare-email.ts
2
+ function layout(title, body, appName) {
3
+ return `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>${title}</title>
7
+ <style>
8
+ body{margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
9
+ .wrap{max-width:480px;margin:40px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08)}
10
+ .header{background:#18181b;padding:24px 32px;color:#fff;font-size:18px;font-weight:600}
11
+ .body{padding:32px}
12
+ .body p{margin:0 0 16px;color:#3f3f46;line-height:1.6;font-size:15px}
13
+ .btn{display:inline-block;padding:12px 28px;background:#18181b;color:#fff!important;text-decoration:none;border-radius:8px;font-weight:500;font-size:15px}
14
+ .code{display:inline-block;padding:12px 24px;background:#f4f4f5;border-radius:8px;font-size:28px;font-weight:700;letter-spacing:6px;color:#18181b}
15
+ .footer{padding:16px 32px;font-size:12px;color:#a1a1aa;border-top:1px solid #f4f4f5}
16
+ </style></head>
17
+ <body><div class="wrap">
18
+ <div class="header">${appName}</div>
19
+ <div class="body">${body}</div>
20
+ <div class="footer">You received this email because an action was requested on your account. If you didn't request this, you can safely ignore it.</div>
21
+ </div></body></html>`;
22
+ }
23
+ var defaultTemplates = {
24
+ verifyEmail: ({ appName, url, userName }) => ({
25
+ subject: `Verify your email \u2013 ${appName}`,
26
+ html: layout(
27
+ "Verify Email",
28
+ `<p>Hi${userName ? ` ${userName}` : ""},</p>
29
+ <p>Thanks for signing up. Please verify your email address by clicking the button below.</p>
30
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Verify Email</a></p>
31
+ <p style="font-size:13px;color:#71717a">If the button doesn't work, copy and paste this link into your browser:<br>${url}</p>`,
32
+ appName
33
+ ),
34
+ text: `Verify your email for ${appName}: ${url}`
35
+ }),
36
+ resetPassword: ({ appName, url, userName }) => ({
37
+ subject: `Reset your password \u2013 ${appName}`,
38
+ html: layout(
39
+ "Reset Password",
40
+ `<p>Hi${userName ? ` ${userName}` : ""},</p>
41
+ <p>We received a request to reset your password. Click the button below to choose a new one.</p>
42
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Reset Password</a></p>
43
+ <p style="font-size:13px;color:#71717a">This link expires in 1 hour. If you didn't request a password reset, you can ignore this email.</p>`,
44
+ appName
45
+ ),
46
+ text: `Reset your password for ${appName}: ${url}`
47
+ }),
48
+ changeEmail: ({ appName, url }) => ({
49
+ subject: `Confirm email change \u2013 ${appName}`,
50
+ html: layout(
51
+ "Confirm Email Change",
52
+ `<p>You requested to change the email address on your ${appName} account.</p>
53
+ <p>Click the button below to confirm this change.</p>
54
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Confirm Change</a></p>
55
+ <p style="font-size:13px;color:#71717a">If you didn't request this, please secure your account immediately.</p>`,
56
+ appName
57
+ ),
58
+ text: `Confirm email change for ${appName}: ${url}`
59
+ }),
60
+ deleteAccount: ({ appName, url }) => ({
61
+ subject: `Confirm account deletion \u2013 ${appName}`,
62
+ html: layout(
63
+ "Delete Account",
64
+ `<p>You requested to delete your ${appName} account. This action is permanent.</p>
65
+ <p>Click the button below to confirm deletion.</p>
66
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn" style="background:#dc2626">Delete Account</a></p>
67
+ <p style="font-size:13px;color:#71717a">If you didn't request this, please ignore this email and secure your account.</p>`,
68
+ appName
69
+ ),
70
+ text: `Confirm account deletion for ${appName}: ${url}`
71
+ }),
72
+ magicLink: ({ appName, url }) => ({
73
+ subject: `Sign in to ${appName}`,
74
+ html: layout(
75
+ "Magic Link",
76
+ `<p>Click the button below to sign in to your ${appName} account. This link expires in 15 minutes.</p>
77
+ <p style="text-align:center;margin:24px 0"><a href="${url}" class="btn">Sign In</a></p>
78
+ <p style="font-size:13px;color:#71717a">If you didn't request this, you can safely ignore this email.</p>`,
79
+ appName
80
+ ),
81
+ text: `Sign in to ${appName}: ${url}`
82
+ }),
83
+ otp: ({ appName, otp }) => ({
84
+ subject: `Your verification code \u2013 ${appName}`,
85
+ html: layout(
86
+ "Verification Code",
87
+ `<p>Use the following code to continue. It expires in 5 minutes.</p>
88
+ <p style="text-align:center;margin:24px 0"><span class="code">${otp}</span></p>
89
+ <p style="font-size:13px;color:#71717a">If you didn't request this code, you can safely ignore this email.</p>`,
90
+ appName
91
+ ),
92
+ text: `Your ${appName} verification code: ${otp}`
93
+ })
94
+ };
95
+ function createWorkersTransport(opts) {
96
+ return {
97
+ async send(message) {
98
+ const binding = typeof opts.binding === "function" ? opts.binding() : opts.binding;
99
+ return binding.send(message);
100
+ }
101
+ };
102
+ }
103
+ function createApiTransport(opts) {
104
+ const baseUrl = opts.baseUrl ?? "https://api.cloudflare.com/client/v4";
105
+ const url = `${baseUrl}/accounts/${opts.accountId}/email-service/send`;
106
+ return {
107
+ async send(message) {
108
+ const res = await fetch(url, {
109
+ method: "POST",
110
+ headers: {
111
+ Authorization: `Bearer ${opts.apiToken}`,
112
+ "Content-Type": "application/json"
113
+ },
114
+ body: JSON.stringify(message)
115
+ });
116
+ if (!res.ok) {
117
+ const body = await res.text();
118
+ throw new Error(`Cloudflare Email API error (${res.status}): ${body}`);
119
+ }
120
+ const json = await res.json();
121
+ return { messageId: json.result?.messageId ?? "" };
122
+ }
123
+ };
124
+ }
125
+ function build(transport, shared) {
126
+ const appName = shared.appName ?? "Our App";
127
+ const from = shared.from;
128
+ const t = { ...defaultTemplates, ...shared.templates };
129
+ function fire(to, rendered) {
130
+ void transport.send({ to, from, subject: rendered.subject, html: rendered.html, text: rendered.text });
131
+ }
132
+ const sendVerificationEmail = async (data, _request) => {
133
+ fire(data.user.email, t.verifyEmail({ appName, url: data.url, token: data.token, userName: data.user.name }));
134
+ };
135
+ const sendResetPassword = async (data, _request) => {
136
+ fire(data.user.email, t.resetPassword({ appName, url: data.url, token: data.token, userName: data.user.name }));
137
+ };
138
+ const sendChangeEmailConfirmation = async (data, _request) => {
139
+ fire(data.newEmail, t.changeEmail({ appName, url: data.url, token: data.token }));
140
+ };
141
+ const sendDeleteAccountVerification = async (data, _request) => {
142
+ fire(data.user.email, t.deleteAccount({ appName, url: data.url, token: data.token }));
143
+ };
144
+ const sendMagicLink = async (data) => {
145
+ fire(data.email, t.magicLink({ appName, url: data.url, token: data.token, email: data.email }));
146
+ };
147
+ const sendVerificationOTP = async (data) => {
148
+ fire(data.email, t.otp({ appName, otp: data.otp, email: data.email }));
149
+ };
150
+ return {
151
+ config: {
152
+ emailVerification: { sendVerificationEmail, sendOnSignUp: true },
153
+ emailAndPassword: { enabled: true, sendResetPassword },
154
+ user: {
155
+ changeEmail: { enabled: true, sendChangeEmailConfirmation },
156
+ deleteUser: { enabled: true, sendDeleteAccountVerification }
157
+ }
158
+ },
159
+ sendVerificationEmail,
160
+ sendResetPassword,
161
+ sendChangeEmailConfirmation,
162
+ sendDeleteAccountVerification,
163
+ sendMagicLink,
164
+ sendVerificationOTP,
165
+ sendRaw: (message) => transport.send(message)
166
+ };
167
+ }
168
+ var cloudflareEmail = {
169
+ /**
170
+ * Use inside a Cloudflare Worker — sends via the `send_email` binding.
171
+ * Zero network hop, no API key needed.
172
+ *
173
+ * ```ts
174
+ * const email = cloudflareEmail.workers({
175
+ * binding: env.EMAIL,
176
+ * from: "App <noreply@app.com>",
177
+ * });
178
+ * ```
179
+ */
180
+ workers(opts) {
181
+ return build(createWorkersTransport(opts), opts);
182
+ },
183
+ /**
184
+ * Use from any runtime (Node.js, Bun, Deno, Vercel, etc.) — sends via
185
+ * the Cloudflare Email Service REST API.
186
+ *
187
+ * ```ts
188
+ * const email = cloudflareEmail.api({
189
+ * accountId: process.env.CF_ACCOUNT_ID,
190
+ * apiToken: process.env.CF_API_TOKEN,
191
+ * from: "App <noreply@app.com>",
192
+ * });
193
+ * ```
194
+ */
195
+ api(opts) {
196
+ return build(createApiTransport(opts), opts);
197
+ }
198
+ };
199
+ export {
200
+ cloudflareEmail
201
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "better-auth-cloudflare-email",
3
+ "version": "0.1.0",
4
+ "description": "Send emails through Cloudflare Email Service from Better Auth — Workers binding and REST API transports",
5
+ "license": "MIT",
6
+ "author": "Paul Stenhouse",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/paulstenhouse/better-auth-cloudflare-email-plugin"
10
+ },
11
+ "keywords": [
12
+ "better-auth",
13
+ "cloudflare",
14
+ "email",
15
+ "cloudflare-workers",
16
+ "cloudflare-email",
17
+ "authentication",
18
+ "transactional-email"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "devDependencies": {
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^5.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "better-auth": ">=1.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "better-auth": {
49
+ "optional": true
50
+ }
51
+ }
52
+ }