better-auth-lead 0.0.1-dev.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/README.md +44 -0
- package/dist/client.d.mts +11 -0
- package/dist/client.mjs +11 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +90 -0
- package/dist/index.mjs +208 -0
- package/dist/index.mjs.map +1 -0
- package/dist/type-CedeOvZ6.d.mts +98 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# better-auth-lead
|
|
2
|
+
|
|
3
|
+
Better Auth plugin to add lead table and API for newsletter/waitlist functionality.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm install better-auth-lead
|
|
10
|
+
# pnpm
|
|
11
|
+
pnpm add better-auth-lead
|
|
12
|
+
# yarn
|
|
13
|
+
yarn add better-auth-lead
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// server/auth.ts
|
|
20
|
+
import { createBetterAuth } from '@better-auth/core';
|
|
21
|
+
import { lead } from 'better-auth-lead';
|
|
22
|
+
|
|
23
|
+
const betterAuth = createBetterAuth({
|
|
24
|
+
plugins: [lead()],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run better auth migration to create the lead table:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx auth@latest generate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Add the lead plugin to your auth client:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// client/auth-client.ts
|
|
38
|
+
import { createAuthClient } from 'better-auth/client';
|
|
39
|
+
import { leadClient } from 'better-auth-lead/client';
|
|
40
|
+
|
|
41
|
+
const authClient = createAuthClient({
|
|
42
|
+
plugins: [leadClient()],
|
|
43
|
+
});
|
|
44
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./type-CedeOvZ6.mjs";
|
|
2
|
+
import { lead } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
declare const leadClient: () => {
|
|
6
|
+
id: "lead";
|
|
7
|
+
$InferServerPlugin: ReturnType<typeof lead>;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { leadClient };
|
|
11
|
+
//# sourceMappingURL=client.d.mts.map
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\nimport type { lead } from './index';\n\nexport const leadClient = () => {\n return {\n id: 'lead',\n $InferServerPlugin: {} as ReturnType<typeof lead>,\n } satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";AAGA,MAAa,mBAAmB;AAC9B,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EACvB"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { n as LeadOptions, r as LeadPayload, t as Lead } from "./type-CedeOvZ6.mjs";
|
|
2
|
+
import * as better_auth0 from "better-auth";
|
|
3
|
+
import * as zod from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
declare const lead: <O extends LeadOptions>(options: O) => {
|
|
7
|
+
id: "lead";
|
|
8
|
+
schema: {
|
|
9
|
+
lead: {
|
|
10
|
+
fields: {
|
|
11
|
+
createdAt: {
|
|
12
|
+
type: "date";
|
|
13
|
+
defaultValue: () => Date;
|
|
14
|
+
required: true;
|
|
15
|
+
input: false;
|
|
16
|
+
};
|
|
17
|
+
updatedAt: {
|
|
18
|
+
type: "date";
|
|
19
|
+
defaultValue: () => Date;
|
|
20
|
+
onUpdate: () => Date;
|
|
21
|
+
required: true;
|
|
22
|
+
input: false;
|
|
23
|
+
};
|
|
24
|
+
email: {
|
|
25
|
+
type: "string";
|
|
26
|
+
required: true;
|
|
27
|
+
unique: true;
|
|
28
|
+
};
|
|
29
|
+
emailVerified: {
|
|
30
|
+
type: "boolean";
|
|
31
|
+
defaultValue: false;
|
|
32
|
+
required: true;
|
|
33
|
+
input: false;
|
|
34
|
+
};
|
|
35
|
+
metadata: {
|
|
36
|
+
type: "string";
|
|
37
|
+
required: false;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
endpoints: {
|
|
43
|
+
subscribe: better_auth0.StrictEndpoint<"/lead/subscribe", {
|
|
44
|
+
method: "POST";
|
|
45
|
+
body: zod.ZodObject<{
|
|
46
|
+
email: zod.ZodEmail;
|
|
47
|
+
metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
|
|
48
|
+
}, better_auth0.$strip>;
|
|
49
|
+
metadata: {};
|
|
50
|
+
}, {
|
|
51
|
+
status: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
verify: better_auth0.StrictEndpoint<"/lead/verify", {
|
|
54
|
+
method: "GET";
|
|
55
|
+
query: zod.ZodObject<{
|
|
56
|
+
token: zod.ZodString;
|
|
57
|
+
}, better_auth0.$strip>;
|
|
58
|
+
metadata: {};
|
|
59
|
+
}, {
|
|
60
|
+
status: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
unsubscribe: better_auth0.StrictEndpoint<"/lead/unsubscribe", {
|
|
63
|
+
method: "POST";
|
|
64
|
+
body: zod.ZodObject<{
|
|
65
|
+
id: zod.ZodString;
|
|
66
|
+
}, better_auth0.$strip>;
|
|
67
|
+
metadata: {};
|
|
68
|
+
}, {
|
|
69
|
+
status: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
resend: better_auth0.StrictEndpoint<"/lead/resend", {
|
|
72
|
+
method: "POST";
|
|
73
|
+
body: zod.ZodObject<{
|
|
74
|
+
email: zod.ZodEmail;
|
|
75
|
+
}, better_auth0.$strip>;
|
|
76
|
+
metadata: {};
|
|
77
|
+
}, {
|
|
78
|
+
status: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
options: NoInfer<O>;
|
|
82
|
+
rateLimit: {
|
|
83
|
+
pathMatcher: (path: string) => boolean;
|
|
84
|
+
window: number;
|
|
85
|
+
max: number;
|
|
86
|
+
}[];
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
export { Lead, LeadOptions, LeadPayload, lead };
|
|
90
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import "better-auth";
|
|
2
|
+
import { mergeSchema } from "better-auth/db";
|
|
3
|
+
import { APIError, createAuthEndpoint, createEmailVerificationToken } from "better-auth/api";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { jwtVerify } from "jose";
|
|
6
|
+
import { JWTExpired } from "jose/errors";
|
|
7
|
+
|
|
8
|
+
//#region src/schema.ts
|
|
9
|
+
const lead$1 = { lead: { fields: {
|
|
10
|
+
createdAt: {
|
|
11
|
+
type: "date",
|
|
12
|
+
defaultValue: () => /* @__PURE__ */ new Date(),
|
|
13
|
+
required: true,
|
|
14
|
+
input: false
|
|
15
|
+
},
|
|
16
|
+
updatedAt: {
|
|
17
|
+
type: "date",
|
|
18
|
+
defaultValue: () => /* @__PURE__ */ new Date(),
|
|
19
|
+
onUpdate: () => /* @__PURE__ */ new Date(),
|
|
20
|
+
required: true,
|
|
21
|
+
input: false
|
|
22
|
+
},
|
|
23
|
+
email: {
|
|
24
|
+
type: "string",
|
|
25
|
+
required: true,
|
|
26
|
+
unique: true
|
|
27
|
+
},
|
|
28
|
+
emailVerified: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
defaultValue: false,
|
|
31
|
+
required: true,
|
|
32
|
+
input: false
|
|
33
|
+
},
|
|
34
|
+
metadata: {
|
|
35
|
+
type: "string",
|
|
36
|
+
required: false
|
|
37
|
+
}
|
|
38
|
+
} } };
|
|
39
|
+
const getSchema = (options) => {
|
|
40
|
+
return mergeSchema(lead$1, options.schema);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/routes.ts
|
|
45
|
+
const subscribeSchema = z.object({
|
|
46
|
+
email: z.email(),
|
|
47
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
48
|
+
});
|
|
49
|
+
const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: subscribeSchema,
|
|
52
|
+
metadata: {}
|
|
53
|
+
}, async (ctx) => {
|
|
54
|
+
const { email, metadata } = ctx.body;
|
|
55
|
+
const normalizedEmail = email.toLowerCase();
|
|
56
|
+
let lead = await ctx.context.adapter.findOne({
|
|
57
|
+
model: "lead",
|
|
58
|
+
where: [{
|
|
59
|
+
field: "email",
|
|
60
|
+
value: normalizedEmail
|
|
61
|
+
}]
|
|
62
|
+
});
|
|
63
|
+
if (!lead) try {
|
|
64
|
+
lead = await ctx.context.adapter.create({
|
|
65
|
+
model: "lead",
|
|
66
|
+
data: {
|
|
67
|
+
email: normalizedEmail,
|
|
68
|
+
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} catch (e) {
|
|
72
|
+
ctx.context.logger.info("Error creating lead");
|
|
73
|
+
lead = await ctx.context.adapter.findOne({
|
|
74
|
+
model: "lead",
|
|
75
|
+
where: [{
|
|
76
|
+
field: "email",
|
|
77
|
+
value: normalizedEmail
|
|
78
|
+
}]
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (!lead.emailVerified) lead = await ctx.context.adapter.update({
|
|
82
|
+
model: "lead",
|
|
83
|
+
where: [{
|
|
84
|
+
field: "email",
|
|
85
|
+
value: normalizedEmail
|
|
86
|
+
}],
|
|
87
|
+
update: { metadata: metadata ? JSON.stringify(metadata) : lead.metadata }
|
|
88
|
+
});
|
|
89
|
+
if (options.sendVerificationEmail && lead && !lead.emailVerified) {
|
|
90
|
+
const token = await createEmailVerificationToken(ctx.context.secret, normalizedEmail, void 0, options.expiresIn ?? 3600);
|
|
91
|
+
const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
|
|
92
|
+
await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
|
|
93
|
+
email: normalizedEmail,
|
|
94
|
+
url,
|
|
95
|
+
token
|
|
96
|
+
}, ctx.request));
|
|
97
|
+
}
|
|
98
|
+
return ctx.json({ status: true });
|
|
99
|
+
});
|
|
100
|
+
const verifySchema = z.object({ token: z.string() });
|
|
101
|
+
const verify = (options) => createAuthEndpoint("/lead/verify", {
|
|
102
|
+
method: "GET",
|
|
103
|
+
query: verifySchema,
|
|
104
|
+
metadata: {}
|
|
105
|
+
}, async (ctx) => {
|
|
106
|
+
const { token } = ctx.query;
|
|
107
|
+
let jwt;
|
|
108
|
+
try {
|
|
109
|
+
jwt = await jwtVerify(token, new TextEncoder().encode(ctx.context.secret), { algorithms: ["HS256"] });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (e instanceof JWTExpired) throw new APIError("UNAUTHORIZED", { message: "Token expired" });
|
|
112
|
+
throw new APIError("UNAUTHORIZED", { message: "Invalid token" });
|
|
113
|
+
}
|
|
114
|
+
const parsed = subscribeSchema.parse(jwt.payload);
|
|
115
|
+
const lead = await ctx.context.adapter.findOne({
|
|
116
|
+
model: "lead",
|
|
117
|
+
where: [{
|
|
118
|
+
field: "email",
|
|
119
|
+
value: parsed.email
|
|
120
|
+
}]
|
|
121
|
+
});
|
|
122
|
+
if (!lead) return ctx.json({ status: true });
|
|
123
|
+
if (lead.emailVerified) return ctx.json({ status: true });
|
|
124
|
+
await ctx.context.adapter.update({
|
|
125
|
+
model: "lead",
|
|
126
|
+
where: [{
|
|
127
|
+
field: "email",
|
|
128
|
+
value: parsed.email
|
|
129
|
+
}],
|
|
130
|
+
update: { emailVerified: true }
|
|
131
|
+
});
|
|
132
|
+
return ctx.json({ status: true });
|
|
133
|
+
});
|
|
134
|
+
const unsubscribeSchema = z.object({ id: z.string() });
|
|
135
|
+
const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: unsubscribeSchema,
|
|
138
|
+
metadata: {}
|
|
139
|
+
}, async (ctx) => {
|
|
140
|
+
const { id } = ctx.body;
|
|
141
|
+
if (!await ctx.context.adapter.findOne({
|
|
142
|
+
model: "lead",
|
|
143
|
+
where: [{
|
|
144
|
+
field: "id",
|
|
145
|
+
value: id
|
|
146
|
+
}]
|
|
147
|
+
})) return ctx.json({ status: true });
|
|
148
|
+
await ctx.context.adapter.delete({
|
|
149
|
+
model: "lead",
|
|
150
|
+
where: [{
|
|
151
|
+
field: "id",
|
|
152
|
+
value: id
|
|
153
|
+
}]
|
|
154
|
+
});
|
|
155
|
+
return ctx.json({ status: true });
|
|
156
|
+
});
|
|
157
|
+
const resendSchema = z.object({ email: z.email() });
|
|
158
|
+
const resend = (options) => createAuthEndpoint("/lead/resend", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: resendSchema,
|
|
161
|
+
metadata: {}
|
|
162
|
+
}, async (ctx) => {
|
|
163
|
+
const { email } = ctx.body;
|
|
164
|
+
const normalizedEmail = email.toLowerCase();
|
|
165
|
+
const lead = await ctx.context.adapter.findOne({
|
|
166
|
+
model: "lead",
|
|
167
|
+
where: [{
|
|
168
|
+
field: "email",
|
|
169
|
+
value: normalizedEmail
|
|
170
|
+
}]
|
|
171
|
+
});
|
|
172
|
+
if (!lead) return ctx.json({ status: true });
|
|
173
|
+
if (options.sendVerificationEmail && lead && !lead.emailVerified) {
|
|
174
|
+
const token = await createEmailVerificationToken(ctx.context.secret, normalizedEmail, void 0, options.expiresIn ?? 3600);
|
|
175
|
+
const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
|
|
176
|
+
await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
|
|
177
|
+
email: normalizedEmail,
|
|
178
|
+
url,
|
|
179
|
+
token
|
|
180
|
+
}, ctx.request));
|
|
181
|
+
}
|
|
182
|
+
return ctx.json({ status: true });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/index.ts
|
|
187
|
+
const lead = (options) => {
|
|
188
|
+
return {
|
|
189
|
+
id: "lead",
|
|
190
|
+
schema: getSchema(options),
|
|
191
|
+
endpoints: {
|
|
192
|
+
subscribe: subscribe(options),
|
|
193
|
+
verify: verify(options),
|
|
194
|
+
unsubscribe: unsubscribe(options),
|
|
195
|
+
resend: resend(options)
|
|
196
|
+
},
|
|
197
|
+
options,
|
|
198
|
+
rateLimit: [{
|
|
199
|
+
pathMatcher: (path) => ["/lead/subscribe", "/lead/resend"].includes(path),
|
|
200
|
+
window: options.rateLimit?.window ?? 10,
|
|
201
|
+
max: options.rateLimit?.max ?? 3
|
|
202
|
+
}]
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
export { lead };
|
|
208
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["lead"],"sources":["../src/schema.ts","../src/routes.ts","../src/index.ts"],"sourcesContent":["import { type BetterAuthPluginDBSchema } from 'better-auth';\nimport type { LeadOptions } from './type';\nimport { mergeSchema } from 'better-auth/db';\n\nexport const lead = {\n lead: {\n fields: {\n createdAt: {\n type: 'date',\n defaultValue: () => new Date(),\n required: true,\n input: false,\n },\n updatedAt: {\n type: 'date',\n defaultValue: () => new Date(),\n onUpdate: () => new Date(),\n required: true,\n input: false,\n },\n email: {\n type: 'string',\n required: true,\n unique: true,\n },\n emailVerified: {\n type: 'boolean',\n defaultValue: false,\n required: true,\n input: false,\n },\n metadata: {\n type: 'string',\n required: false,\n },\n },\n },\n} satisfies BetterAuthPluginDBSchema;\n\nexport const getSchema = <O extends LeadOptions>(options: O) => {\n return mergeSchema(lead, options.schema);\n};\n","import { APIError, createAuthEndpoint, createEmailVerificationToken } from 'better-auth/api';\nimport * as z from 'zod';\nimport type { Lead, LeadOptions, LeadPayload } from './type';\nimport { jwtVerify } from 'jose';\nimport type { JWTPayload, JWTVerifyResult } from 'jose';\nimport { JWTExpired } from 'jose/errors';\n\nconst subscribeSchema = z.object({\n email: z.email(),\n metadata: z.record(z.string(), z.any()).optional(),\n});\n\nexport const subscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/subscribe',\n {\n method: 'POST',\n body: subscribeSchema,\n metadata: {\n // TODO add openapi\n },\n },\n async (ctx) => {\n const { email, metadata } = ctx.body;\n\n const normalizedEmail = email.toLowerCase();\n\n let lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: normalizedEmail,\n },\n ],\n });\n\n if (!lead) {\n try {\n lead = await ctx.context.adapter.create<LeadPayload, Lead>({\n model: 'lead',\n data: {\n email: normalizedEmail,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n },\n });\n } catch (e) {\n ctx.context.logger.info('Error creating lead');\n lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: normalizedEmail,\n },\n ],\n });\n }\n } else if (!lead.emailVerified) {\n lead = await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: normalizedEmail,\n },\n ],\n update: {\n metadata: metadata ? JSON.stringify(metadata) : lead.metadata,\n },\n });\n }\n\n if (options.sendVerificationEmail && lead && !lead.emailVerified) {\n const token = await createEmailVerificationToken(\n ctx.context.secret,\n normalizedEmail,\n undefined,\n options.expiresIn ?? 3600,\n );\n const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;\n\n await ctx.context.runInBackgroundOrAwait(\n options.sendVerificationEmail({ email: normalizedEmail, url, token }, ctx.request),\n );\n }\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst verifySchema = z.object({\n token: z.string(),\n});\n\nexport const verify = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/verify',\n {\n method: 'GET',\n query: verifySchema,\n metadata: {\n // TODO add openapi\n },\n },\n async (ctx) => {\n const { token } = ctx.query;\n\n let jwt: JWTVerifyResult<JWTPayload>;\n try {\n jwt = await jwtVerify(token, new TextEncoder().encode(ctx.context.secret), {\n algorithms: ['HS256'],\n });\n } catch (e) {\n if (e instanceof JWTExpired) {\n throw new APIError('UNAUTHORIZED', {\n message: 'Token expired',\n });\n }\n throw new APIError('UNAUTHORIZED', {\n message: 'Invalid token',\n });\n }\n\n const parsed = subscribeSchema.parse(jwt.payload);\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: parsed.email,\n },\n ],\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (lead.emailVerified) {\n return ctx.json({\n status: true,\n });\n }\n\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: parsed.email,\n },\n ],\n update: {\n emailVerified: true,\n },\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst unsubscribeSchema = z.object({\n id: z.string(),\n});\n\nexport const unsubscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe',\n {\n method: 'POST',\n body: unsubscribeSchema,\n metadata: {\n // TODO add openapi\n },\n },\n async (ctx) => {\n const { id } = ctx.body;\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [\n {\n field: 'id',\n value: id,\n },\n ],\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n await ctx.context.adapter.delete({\n model: 'lead',\n where: [\n {\n field: 'id',\n value: id,\n },\n ],\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst resendSchema = z.object({\n email: z.email(),\n});\n\nexport const resend = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/resend',\n {\n method: 'POST',\n body: resendSchema,\n metadata: {\n // TODO add openapi\n },\n },\n async (ctx) => {\n const { email } = ctx.body;\n\n const normalizedEmail = email.toLowerCase();\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [\n {\n field: 'email',\n value: normalizedEmail,\n },\n ],\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (options.sendVerificationEmail && lead && !lead.emailVerified) {\n const token = await createEmailVerificationToken(\n ctx.context.secret,\n normalizedEmail,\n undefined,\n options.expiresIn ?? 3600,\n );\n const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;\n\n await ctx.context.runInBackgroundOrAwait(\n options.sendVerificationEmail({ email: normalizedEmail, url, token }, ctx.request),\n );\n }\n\n return ctx.json({\n status: true,\n });\n },\n );\n","import type { BetterAuthPlugin } from 'better-auth';\nimport type { LeadOptions } from './type';\nimport { getSchema } from './schema';\nimport { resend, subscribe, unsubscribe, verify } from './routes';\n\nexport const lead = <O extends LeadOptions>(options: O) => {\n return {\n id: 'lead',\n schema: getSchema(options),\n endpoints: {\n subscribe: subscribe(options),\n verify: verify(options),\n unsubscribe: unsubscribe(options),\n resend: resend(options),\n },\n options: options as NoInfer<O>,\n rateLimit: [\n {\n pathMatcher: (path) => ['/lead/subscribe', '/lead/resend'].includes(path),\n window: options.rateLimit?.window ?? 10,\n max: options.rateLimit?.max ?? 3,\n },\n ],\n } satisfies BetterAuthPlugin;\n};\n\nexport type * from './type';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { InferOptionSchema } from "better-auth";
|
|
2
|
+
|
|
3
|
+
//#region src/schema.d.ts
|
|
4
|
+
declare const lead: {
|
|
5
|
+
lead: {
|
|
6
|
+
fields: {
|
|
7
|
+
createdAt: {
|
|
8
|
+
type: "date";
|
|
9
|
+
defaultValue: () => Date;
|
|
10
|
+
required: true;
|
|
11
|
+
input: false;
|
|
12
|
+
};
|
|
13
|
+
updatedAt: {
|
|
14
|
+
type: "date";
|
|
15
|
+
defaultValue: () => Date;
|
|
16
|
+
onUpdate: () => Date;
|
|
17
|
+
required: true;
|
|
18
|
+
input: false;
|
|
19
|
+
};
|
|
20
|
+
email: {
|
|
21
|
+
type: "string";
|
|
22
|
+
required: true;
|
|
23
|
+
unique: true;
|
|
24
|
+
};
|
|
25
|
+
emailVerified: {
|
|
26
|
+
type: "boolean";
|
|
27
|
+
defaultValue: false;
|
|
28
|
+
required: true;
|
|
29
|
+
input: false;
|
|
30
|
+
};
|
|
31
|
+
metadata: {
|
|
32
|
+
type: "string";
|
|
33
|
+
required: false;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/type.d.ts
|
|
40
|
+
interface LeadOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Send a verification email
|
|
43
|
+
* @param data the data object
|
|
44
|
+
* @param request the request object
|
|
45
|
+
*/
|
|
46
|
+
sendVerificationEmail?: (
|
|
47
|
+
/**
|
|
48
|
+
* @param email the email to send the verification email to
|
|
49
|
+
* @param url the verification url
|
|
50
|
+
* @param token the verification token
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
data: {
|
|
54
|
+
email: string;
|
|
55
|
+
url: string;
|
|
56
|
+
token: string;
|
|
57
|
+
}, request?: Request) => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Number of seconds the verification token is
|
|
60
|
+
* valid for.
|
|
61
|
+
* @default 3600 seconds (1 hour)
|
|
62
|
+
*/
|
|
63
|
+
expiresIn?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Rate limit configuration for /lead/subscribe and /lead/resend endpoints.
|
|
66
|
+
*/
|
|
67
|
+
rateLimit?: {
|
|
68
|
+
/**
|
|
69
|
+
* Time window in seconds for which the rate limit applies.
|
|
70
|
+
* @default 10 seconds
|
|
71
|
+
*/
|
|
72
|
+
window: number;
|
|
73
|
+
/**
|
|
74
|
+
* Maximum number of requests allowed within the time window.
|
|
75
|
+
* @default 3 requests
|
|
76
|
+
*/
|
|
77
|
+
max: number;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Schema for the lead plugin
|
|
81
|
+
*/
|
|
82
|
+
schema?: InferOptionSchema<typeof lead> | undefined;
|
|
83
|
+
}
|
|
84
|
+
interface Lead {
|
|
85
|
+
/**
|
|
86
|
+
* Database identifier
|
|
87
|
+
*/
|
|
88
|
+
id: string;
|
|
89
|
+
createdAt: Date;
|
|
90
|
+
updatedAt: Date;
|
|
91
|
+
email: string;
|
|
92
|
+
emailVerified: boolean;
|
|
93
|
+
metadata?: string;
|
|
94
|
+
}
|
|
95
|
+
type LeadPayload = Omit<Lead, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified'>;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { LeadOptions as n, LeadPayload as r, Lead as t };
|
|
98
|
+
//# sourceMappingURL=type-CedeOvZ6.d.mts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-auth-lead",
|
|
3
|
+
"version": "0.0.1-dev.0",
|
|
4
|
+
"description": "Better Auth Lead plugin",
|
|
5
|
+
"homepage": "https://github.com/marcjulian/better-auth-plugins#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/marcjulian/better-auth-plugins/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Marc Stammerjohann <me@marcjulian.de>",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/marcjulian/better-auth-plugins.git"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.mjs",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.mjs",
|
|
24
|
+
"./client": "./dist/client.mjs",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"jose": "^6.1.3",
|
|
29
|
+
"zod": "^4.3.6"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.3.0",
|
|
33
|
+
"bumpp": "^10.4.1",
|
|
34
|
+
"tsdown": "^0.20.3",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"better-auth": "^1.5.1"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|