better-auth-lead 0.0.1-dev.2 → 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/README.md CHANGED
@@ -11,6 +11,8 @@ npm install better-auth-lead
11
11
  pnpm add better-auth-lead
12
12
  # yarn
13
13
  yarn add better-auth-lead
14
+ # bun
15
+ bun add better-auth-lead
14
16
  ```
15
17
 
16
18
  Add the plugin to your auth config
@@ -28,7 +30,14 @@ const betterAuth = createBetterAuth({
28
30
  Run better auth migration to create the lead table:
29
31
 
30
32
  ```bash
33
+ # npm
31
34
  npx auth@latest generate
35
+ # pnpm
36
+ pnpm dlx auth@latest generate
37
+ # yarn
38
+ yarn dlx auth@latest generate
39
+ # bun
40
+ bun x auth@latest generate
32
41
  ```
33
42
 
34
43
  Add the lead plugin to your auth client:
@@ -99,6 +108,14 @@ const { data, error } = await authClient.lead.update({
99
108
 
100
109
  ### Email Verification
101
110
 
111
+ To enable email verification, you need to pass a function that sends a verification email with a link. The `sendVerificationEmail` takes a data object with the following properties:
112
+
113
+ - `email`: The lead email.
114
+ - `url`: The URL to send to the user which contains the token.
115
+ - `token`: A verification token used to complete the email verification.
116
+
117
+ and a `request` object as the second parameter.
118
+
102
119
  ```ts
103
120
  // server/auth.ts
104
121
  import { betterAuth } from 'better-auth';
@@ -115,6 +132,10 @@ export const auth = betterAuth({
115
132
  text: `Click the link to verify your email: ${url}`,
116
133
  });
117
134
  },
135
+ onEmailVerified: async ({ lead }) => {
136
+ // do something when a lead's email is verified
137
+ console.log(`Lead ${lead.email} has been verified!`);
138
+ },
118
139
  }),
119
140
  ],
120
141
  });
@@ -122,6 +143,35 @@ export const auth = betterAuth({
122
143
 
123
144
  > Avoid awaiting the email sending to prevent timing attacks.
124
145
 
146
+ Additionally, you can provide an `onEmailVerified` callback to execute logic after a lead's email is verified.
147
+
148
+ ### Metadata Validation
149
+
150
+ To validate and parse metadata, you can pass a Standard Schema compatible schema (e.g. Zod, Valibot, ArkType).
151
+
152
+ ```ts
153
+ // server/auth.ts
154
+ import { betterAuth } from 'better-auth';
155
+ import { lead } from 'better-auth-lead';
156
+ import * as z from 'zod';
157
+
158
+ const metadataSchema = z.object({
159
+ preferences: z.enum(['engineering', 'marketing', 'design']),
160
+ });
161
+
162
+ export const auth = betterAuth({
163
+ plugins: [
164
+ lead({
165
+ metadata: {
166
+ validationSchema: metadataSchema,
167
+ },
168
+ }),
169
+ ],
170
+ });
171
+ ```
172
+
173
+ If the schema validation fails, the API `subscribe` and `update` routes will return a `400 Bad Request` error with `INVALID_METADATA`.
174
+
125
175
  ## Schema
126
176
 
127
177
  ### Lead
package/dist/client.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import "./type-KTJzD7Eo.mjs";
2
- import { lead } from "./index.mjs";
1
+ import { t as lead } from "./index-B-BhgW_e.mjs";
3
2
  import * as better_auth0 from "better-auth";
4
3
 
5
4
  //#region src/client.d.ts
@@ -10,6 +9,7 @@ declare const leadClient: () => {
10
9
  INVALID_EMAIL: better_auth0.RawError<"INVALID_EMAIL">;
11
10
  INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
12
11
  TOKEN_EXPIRED: better_auth0.RawError<"TOKEN_EXPIRED">;
12
+ INVALID_METADATA: better_auth0.RawError<"INVALID_METADATA">;
13
13
  };
14
14
  };
15
15
  //#endregion
package/dist/client.mjs CHANGED
@@ -1,5 +1,4 @@
1
- import { t as LEAD_ERROR_CODES } from "./error-codes-d1rquQlA.mjs";
2
-
1
+ import { t as LEAD_ERROR_CODES } from "./error-codes-CZUPEOrE.mjs";
3
2
  //#region src/client.ts
4
3
  const leadClient = () => {
5
4
  return {
@@ -8,7 +7,7 @@ const leadClient = () => {
8
7
  $ERROR_CODES: LEAD_ERROR_CODES
9
8
  };
10
9
  };
11
-
12
10
  //#endregion
13
11
  export { leadClient };
12
+
14
13
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport type { lead } from './index';\n\nexport const leadClient = () => {\n return {\n id: 'lead',\n $InferServerPlugin: {} as ReturnType<typeof lead>,\n $ERROR_CODES: LEAD_ERROR_CODES,\n } satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";;;AAKA,MAAa,mBAAmB;AAC9B,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EACtB,cAAc;EACf"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport type { lead } from './index';\n\nexport const leadClient = () => {\n return {\n id: 'lead',\n $InferServerPlugin: {} as ReturnType<typeof lead>,\n $ERROR_CODES: LEAD_ERROR_CODES,\n } satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";;AAKA,MAAa,mBAAmB;AAC9B,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EACtB,cAAc;EACf"}
@@ -1,12 +1,12 @@
1
1
  import { defineErrorCodes } from "better-auth";
2
-
3
2
  //#region src/error-codes.ts
4
3
  const LEAD_ERROR_CODES = defineErrorCodes({
5
4
  INVALID_EMAIL: "Invalid email",
6
5
  INVALID_TOKEN: "Invalid token",
7
- TOKEN_EXPIRED: "Token expired"
6
+ TOKEN_EXPIRED: "Token expired",
7
+ INVALID_METADATA: "Invalid metadata"
8
8
  });
9
-
10
9
  //#endregion
11
10
  export { LEAD_ERROR_CODES as t };
12
- //# sourceMappingURL=error-codes-d1rquQlA.mjs.map
11
+
12
+ //# sourceMappingURL=error-codes-CZUPEOrE.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes-d1rquQlA.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from 'better-auth';\n\nexport const LEAD_ERROR_CODES = defineErrorCodes({\n INVALID_EMAIL: 'Invalid email',\n INVALID_TOKEN: 'Invalid token',\n TOKEN_EXPIRED: 'Token expired',\n});\n"],"mappings":";;;AAEA,MAAa,mBAAmB,iBAAiB;CAC/C,eAAe;CACf,eAAe;CACf,eAAe;CAChB,CAAC"}
1
+ {"version":3,"file":"error-codes-CZUPEOrE.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from 'better-auth';\n\nexport const LEAD_ERROR_CODES = defineErrorCodes({\n INVALID_EMAIL: 'Invalid email',\n INVALID_TOKEN: 'Invalid token',\n TOKEN_EXPIRED: 'Token expired',\n INVALID_METADATA: 'Invalid metadata',\n});\n"],"mappings":";;AAEA,MAAa,mBAAmB,iBAAiB;CAC/C,eAAe;CACf,eAAe;CACf,eAAe;CACf,kBAAkB;CACnB,CAAC"}
@@ -0,0 +1,206 @@
1
+ import * as better_auth0 from "better-auth";
2
+ import { InferOptionSchema, StandardSchemaV1 } from "better-auth";
3
+ import * as zod from "zod";
4
+
5
+ //#region src/schema.d.ts
6
+ declare const lead$1: {
7
+ lead: {
8
+ fields: {
9
+ createdAt: {
10
+ type: "date";
11
+ defaultValue: () => Date;
12
+ required: true;
13
+ input: false;
14
+ };
15
+ updatedAt: {
16
+ type: "date";
17
+ defaultValue: () => Date;
18
+ onUpdate: () => Date;
19
+ required: true;
20
+ input: false;
21
+ };
22
+ email: {
23
+ type: "string";
24
+ required: true;
25
+ unique: true;
26
+ };
27
+ emailVerified: {
28
+ type: "boolean";
29
+ defaultValue: false;
30
+ required: true;
31
+ input: false;
32
+ };
33
+ metadata: {
34
+ type: "string";
35
+ required: false;
36
+ };
37
+ };
38
+ };
39
+ };
40
+ //#endregion
41
+ //#region src/type.d.ts
42
+ interface LeadOptions {
43
+ /**
44
+ * Send a verification email
45
+ * @param data the data object
46
+ * @param request the request object
47
+ */
48
+ sendVerificationEmail?: (
49
+ /**
50
+ * @param email the email to send the verification email to
51
+ * @param url the verification url
52
+ * @param token the verification token
53
+ */
54
+
55
+ data: {
56
+ email: string;
57
+ url: string;
58
+ token: string;
59
+ }, request?: Request) => Promise<void>;
60
+ onEmailVerified?: (
61
+ /**
62
+ * @param lead the lead that was verified
63
+ */
64
+
65
+ data: {
66
+ lead: Lead;
67
+ }, request?: Request) => Promise<void>;
68
+ /**
69
+ * Number of seconds the verification token is
70
+ * valid for.
71
+ * @default 3600 seconds (1 hour)
72
+ */
73
+ expiresIn?: number;
74
+ /**
75
+ * Rate limit configuration for /lead/subscribe and /lead/resend endpoints.
76
+ */
77
+ rateLimit?: {
78
+ /**
79
+ * Time window in seconds for which the rate limit applies.
80
+ * @default 10 seconds
81
+ */
82
+ window: number;
83
+ /**
84
+ * Maximum number of requests allowed within the time window.
85
+ * @default 3 requests
86
+ */
87
+ max: number;
88
+ };
89
+ /**
90
+ * Schema for the lead plugin
91
+ */
92
+ schema?: InferOptionSchema<typeof lead$1> | undefined;
93
+ metadata?: {
94
+ validationSchema?: StandardSchemaV1;
95
+ };
96
+ }
97
+ interface Lead {
98
+ /**
99
+ * Database identifier
100
+ */
101
+ id: string;
102
+ createdAt: Date;
103
+ updatedAt: Date;
104
+ email: string;
105
+ emailVerified: boolean;
106
+ metadata?: string;
107
+ }
108
+ type LeadPayload = Omit<Lead, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified'>;
109
+ //#endregion
110
+ //#region src/index.d.ts
111
+ declare const lead: <O extends LeadOptions>(options?: O) => {
112
+ id: "lead";
113
+ schema: {
114
+ lead: {
115
+ fields: {
116
+ createdAt: {
117
+ type: "date";
118
+ defaultValue: () => Date;
119
+ required: true;
120
+ input: false;
121
+ };
122
+ updatedAt: {
123
+ type: "date";
124
+ defaultValue: () => Date;
125
+ onUpdate: () => Date;
126
+ required: true;
127
+ input: false;
128
+ };
129
+ email: {
130
+ type: "string";
131
+ required: true;
132
+ unique: true;
133
+ };
134
+ emailVerified: {
135
+ type: "boolean";
136
+ defaultValue: false;
137
+ required: true;
138
+ input: false;
139
+ };
140
+ metadata: {
141
+ type: "string";
142
+ required: false;
143
+ };
144
+ };
145
+ };
146
+ };
147
+ endpoints: {
148
+ subscribe: better_auth0.StrictEndpoint<"/lead/subscribe", {
149
+ method: "POST";
150
+ body: zod.ZodObject<{
151
+ email: zod.ZodString;
152
+ metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
153
+ }, better_auth0.$strip>;
154
+ }, {
155
+ status: boolean;
156
+ }>;
157
+ verify: better_auth0.StrictEndpoint<"/lead/verify", {
158
+ method: "GET";
159
+ query: zod.ZodObject<{
160
+ token: zod.ZodString;
161
+ }, better_auth0.$strip>;
162
+ }, {
163
+ status: boolean;
164
+ }>;
165
+ unsubscribe: better_auth0.StrictEndpoint<"/lead/unsubscribe", {
166
+ method: "POST";
167
+ body: zod.ZodObject<{
168
+ id: zod.ZodString;
169
+ }, better_auth0.$strip>;
170
+ }, {
171
+ status: boolean;
172
+ }>;
173
+ resend: better_auth0.StrictEndpoint<"/lead/resend", {
174
+ method: "POST";
175
+ body: zod.ZodObject<{
176
+ email: zod.ZodString;
177
+ }, better_auth0.$strip>;
178
+ }, {
179
+ status: boolean;
180
+ }>;
181
+ update: better_auth0.StrictEndpoint<"/lead/update", {
182
+ method: "POST";
183
+ body: zod.ZodObject<{
184
+ id: zod.ZodString;
185
+ metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
186
+ }, better_auth0.$strip>;
187
+ }, {
188
+ status: boolean;
189
+ }>;
190
+ };
191
+ options: NoInfer<O>;
192
+ rateLimit: {
193
+ pathMatcher: (path: string) => boolean;
194
+ window: number;
195
+ max: number;
196
+ }[];
197
+ $ERROR_CODES: {
198
+ INVALID_EMAIL: better_auth0.RawError<"INVALID_EMAIL">;
199
+ INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
200
+ TOKEN_EXPIRED: better_auth0.RawError<"TOKEN_EXPIRED">;
201
+ INVALID_METADATA: better_auth0.RawError<"INVALID_METADATA">;
202
+ };
203
+ };
204
+ //#endregion
205
+ export { LeadPayload as i, Lead as n, LeadOptions as r, lead as t };
206
+ //# sourceMappingURL=index-B-BhgW_e.d.mts.map
package/dist/index.d.mts CHANGED
@@ -1,100 +1,2 @@
1
- import { n as LeadOptions, r as LeadPayload, t as Lead } from "./type-KTJzD7Eo.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.ZodString;
47
- metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
48
- }, better_auth0.$strip>;
49
- }, {
50
- status: boolean;
51
- }>;
52
- verify: better_auth0.StrictEndpoint<"/lead/verify", {
53
- method: "GET";
54
- query: zod.ZodObject<{
55
- token: zod.ZodString;
56
- }, better_auth0.$strip>;
57
- }, {
58
- status: boolean;
59
- }>;
60
- unsubscribe: better_auth0.StrictEndpoint<"/lead/unsubscribe", {
61
- method: "POST";
62
- body: zod.ZodObject<{
63
- id: zod.ZodString;
64
- }, better_auth0.$strip>;
65
- }, {
66
- status: boolean;
67
- }>;
68
- resend: better_auth0.StrictEndpoint<"/lead/resend", {
69
- method: "POST";
70
- body: zod.ZodObject<{
71
- email: zod.ZodString;
72
- }, better_auth0.$strip>;
73
- }, {
74
- status: boolean;
75
- }>;
76
- update: better_auth0.StrictEndpoint<"/lead/update", {
77
- method: "POST";
78
- body: zod.ZodObject<{
79
- id: zod.ZodString;
80
- metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
81
- }, better_auth0.$strip>;
82
- }, {
83
- status: boolean;
84
- }>;
85
- };
86
- options: NoInfer<O>;
87
- rateLimit: {
88
- pathMatcher: (path: string) => boolean;
89
- window: number;
90
- max: number;
91
- }[];
92
- $ERROR_CODES: {
93
- INVALID_EMAIL: better_auth0.RawError<"INVALID_EMAIL">;
94
- INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
95
- TOKEN_EXPIRED: better_auth0.RawError<"TOKEN_EXPIRED">;
96
- };
97
- };
98
- //#endregion
99
- export { Lead, LeadOptions, LeadPayload, lead };
100
- //# sourceMappingURL=index.d.mts.map
1
+ import { i as LeadPayload, n as Lead, r as LeadOptions, t as lead } from "./index-B-BhgW_e.mjs";
2
+ export { Lead, LeadOptions, LeadPayload, lead };
package/dist/index.mjs CHANGED
@@ -1,11 +1,10 @@
1
- import { t as LEAD_ERROR_CODES } from "./error-codes-d1rquQlA.mjs";
2
- import "better-auth";
1
+ import { t as LEAD_ERROR_CODES } from "./error-codes-CZUPEOrE.mjs";
2
+ import { BASE_ERROR_CODES } from "better-auth";
3
3
  import { APIError, createAuthEndpoint, createEmailVerificationToken } from "better-auth/api";
4
4
  import { jwtVerify } from "jose";
5
5
  import { JWTExpired } from "jose/errors";
6
6
  import * as z from "zod";
7
7
  import { mergeSchema } from "better-auth/db";
8
-
9
8
  //#region src/routes.ts
10
9
  const subscribeSchema = z.object({
11
10
  email: z.string().meta({ description: "Email address of the lead" }),
@@ -15,8 +14,9 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
15
14
  method: "POST",
16
15
  body: subscribeSchema
17
16
  }, async (ctx) => {
18
- const { email, metadata } = ctx.body;
17
+ const { email } = ctx.body;
19
18
  if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
19
+ const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
20
20
  const normalizedEmail = email.toLowerCase();
21
21
  let lead = await ctx.context.adapter.findOne({
22
22
  model: "lead",
@@ -68,7 +68,7 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
68
68
  throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.INVALID_TOKEN);
69
69
  }
70
70
  const parsed = subscribeSchema.parse(jwt.payload);
71
- const lead = await ctx.context.adapter.findOne({
71
+ let lead = await ctx.context.adapter.findOne({
72
72
  model: "lead",
73
73
  where: [{
74
74
  field: "email",
@@ -77,7 +77,7 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
77
77
  });
78
78
  if (!lead) return ctx.json({ status: true });
79
79
  if (lead.emailVerified) return ctx.json({ status: true });
80
- await ctx.context.adapter.update({
80
+ lead = await ctx.context.adapter.update({
81
81
  model: "lead",
82
82
  where: [{
83
83
  field: "email",
@@ -85,6 +85,8 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
85
85
  }],
86
86
  update: { emailVerified: true }
87
87
  });
88
+ if (!lead) return ctx.json({ status: true });
89
+ if (options.onEmailVerified) await ctx.context.runInBackgroundOrAwait(options.onEmailVerified({ lead }, ctx.request));
88
90
  return ctx.json({ status: true });
89
91
  });
90
92
  const unsubscribeSchema = z.object({ id: z.string().meta({ description: "The id of the lead to unsubscribe" }) });
@@ -144,7 +146,7 @@ const update = (options) => createAuthEndpoint("/lead/update", {
144
146
  method: "POST",
145
147
  body: updateSchema
146
148
  }, async (ctx) => {
147
- const { id, metadata } = ctx.body;
149
+ const { id } = ctx.body;
148
150
  if (!await ctx.context.adapter.findOne({
149
151
  model: "lead",
150
152
  where: [{
@@ -152,6 +154,7 @@ const update = (options) => createAuthEndpoint("/lead/update", {
152
154
  value: id
153
155
  }]
154
156
  })) return ctx.json({ status: true });
157
+ const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
155
158
  await ctx.context.adapter.update({
156
159
  model: "lead",
157
160
  where: [{
@@ -162,7 +165,16 @@ const update = (options) => createAuthEndpoint("/lead/update", {
162
165
  });
163
166
  return ctx.json({ status: true });
164
167
  });
165
-
168
+ function validateMetadata(options, metadata, logger) {
169
+ if (!metadata || !options.metadata?.validationSchema) return metadata;
170
+ const validationResult = options.metadata.validationSchema["~standard"].validate(metadata);
171
+ if (validationResult instanceof Promise) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED);
172
+ if (validationResult.issues) {
173
+ logger.error("Invalid metadata", validationResult.issues);
174
+ throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_METADATA);
175
+ }
176
+ return validationResult.value;
177
+ }
166
178
  //#endregion
167
179
  //#region src/schema.ts
168
180
  const lead$1 = { lead: { fields: {
@@ -198,7 +210,6 @@ const lead$1 = { lead: { fields: {
198
210
  const getSchema = (options) => {
199
211
  return mergeSchema(lead$1, options.schema);
200
212
  };
201
-
202
213
  //#endregion
203
214
  //#region src/index.ts
204
215
  const lead = (options = {}) => {
@@ -221,7 +232,7 @@ const lead = (options = {}) => {
221
232
  $ERROR_CODES: LEAD_ERROR_CODES
222
233
  };
223
234
  };
224
-
225
235
  //#endregion
226
236
  export { lead };
237
+
227
238
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["lead"],"sources":["../src/routes.ts","../src/schema.ts","../src/index.ts"],"sourcesContent":["import { APIError, createAuthEndpoint, createEmailVerificationToken } from 'better-auth/api';\nimport { jwtVerify } from 'jose';\nimport type { JWTPayload, JWTVerifyResult } from 'jose';\nimport { JWTExpired } from 'jose/errors';\nimport * as z from 'zod';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport type { Lead, LeadOptions, LeadPayload } from './type';\n\nconst subscribeSchema = z.object({\n email: z.string().meta({\n description: 'Email address of the lead',\n }),\n metadata: z.record(z.string(), z.any()).optional().meta({\n description: 'Additional metadata to store with the lead',\n }),\n});\n\nexport const subscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/subscribe',\n {\n method: 'POST',\n body: subscribeSchema,\n },\n async (ctx) => {\n const { email, metadata } = ctx.body;\n\n const isValidEmail = z.email().safeParse(email);\n if (!isValidEmail.success) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.INVALID_EMAIL);\n }\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 }\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().meta({\n description: 'The token to verify the email',\n }),\n});\n\nexport const verify = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/verify',\n {\n method: 'GET',\n query: verifySchema,\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 APIError.from('UNAUTHORIZED', LEAD_ERROR_CODES.TOKEN_EXPIRED);\n }\n throw APIError.from('UNAUTHORIZED', LEAD_ERROR_CODES.INVALID_TOKEN);\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().meta({\n description: 'The id of the lead to unsubscribe',\n }),\n});\n\nexport const unsubscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe',\n {\n method: 'POST',\n body: unsubscribeSchema,\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.string().meta({\n description: 'Email address to resend the verification email to',\n }),\n});\n\nexport const resend = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/resend',\n {\n method: 'POST',\n body: resendSchema,\n },\n async (ctx) => {\n const { email } = ctx.body;\n\n const isValidEmail = z.email().safeParse(email);\n if (!isValidEmail.success) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.INVALID_EMAIL);\n }\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\nconst updateSchema = z.object({\n id: z.string().meta({\n description: 'The id of the lead to update',\n }),\n metadata: z.record(z.string(), z.any()).optional().meta({\n description: 'Additional metadata to store with the lead',\n }),\n});\n\nexport const update = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/update',\n {\n method: 'POST',\n body: updateSchema,\n },\n async (ctx) => {\n const { id, metadata } = 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.update<Lead>({\n model: 'lead',\n where: [\n {\n field: 'id',\n value: id,\n },\n ],\n update: {\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n },\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n","import { type BetterAuthPluginDBSchema } from 'better-auth';\nimport { mergeSchema } from 'better-auth/db';\n\nimport type { LeadOptions } from './type';\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 type { BetterAuthPlugin } from 'better-auth';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport { resend, subscribe, unsubscribe, update, verify } from './routes';\nimport { getSchema } from './schema';\nimport type { LeadOptions } from './type';\n\nexport const lead = <O extends LeadOptions>(options: O = {} as 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 update: update(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 $ERROR_CODES: LEAD_ERROR_CODES,\n } satisfies BetterAuthPlugin;\n};\n\nexport type * from './type';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["lead"],"sources":["../src/routes.ts","../src/schema.ts","../src/index.ts"],"sourcesContent":["import { BASE_ERROR_CODES, type InternalLogger } from 'better-auth';\nimport { APIError, createAuthEndpoint, createEmailVerificationToken } from 'better-auth/api';\nimport { jwtVerify } from 'jose';\nimport type { JWTPayload, JWTVerifyResult } from 'jose';\nimport { JWTExpired } from 'jose/errors';\nimport * as z from 'zod';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport type { Lead, LeadOptions, LeadPayload } from './type';\n\nconst subscribeSchema = z.object({\n email: z.string().meta({\n description: 'Email address of the lead',\n }),\n metadata: z.record(z.string(), z.any()).optional().meta({\n description: 'Additional metadata to store with the lead',\n }),\n});\n\nexport const subscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/subscribe',\n {\n method: 'POST',\n body: subscribeSchema,\n },\n async (ctx) => {\n const { email } = ctx.body;\n\n const isValidEmail = z.email().safeParse(email);\n if (!isValidEmail.success) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.INVALID_EMAIL);\n }\n\n const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);\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 }\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().meta({\n description: 'The token to verify the email',\n }),\n});\n\nexport const verify = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/verify',\n {\n method: 'GET',\n query: verifySchema,\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 APIError.from('UNAUTHORIZED', LEAD_ERROR_CODES.TOKEN_EXPIRED);\n }\n throw APIError.from('UNAUTHORIZED', LEAD_ERROR_CODES.INVALID_TOKEN);\n }\n\n const parsed = subscribeSchema.parse(jwt.payload);\n\n let 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 lead = 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 if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (options.onEmailVerified) {\n await ctx.context.runInBackgroundOrAwait(options.onEmailVerified({ lead }, ctx.request));\n }\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst unsubscribeSchema = z.object({\n id: z.string().meta({\n description: 'The id of the lead to unsubscribe',\n }),\n});\n\nexport const unsubscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe',\n {\n method: 'POST',\n body: unsubscribeSchema,\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.string().meta({\n description: 'Email address to resend the verification email to',\n }),\n});\n\nexport const resend = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/resend',\n {\n method: 'POST',\n body: resendSchema,\n },\n async (ctx) => {\n const { email } = ctx.body;\n\n const isValidEmail = z.email().safeParse(email);\n if (!isValidEmail.success) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.INVALID_EMAIL);\n }\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\nconst updateSchema = z.object({\n id: z.string().meta({\n description: 'The id of the lead to update',\n }),\n metadata: z.record(z.string(), z.any()).optional().meta({\n description: 'Additional metadata to store with the lead',\n }),\n});\n\nexport const update = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/update',\n {\n method: 'POST',\n body: updateSchema,\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 const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);\n\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [\n {\n field: 'id',\n value: id,\n },\n ],\n update: {\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n },\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nfunction validateMetadata(\n options: LeadOptions,\n metadata: Record<string, any> | undefined,\n logger: InternalLogger,\n) {\n if (!metadata || !options.metadata?.validationSchema) {\n return metadata;\n }\n const validationResult = options.metadata.validationSchema['~standard'].validate(metadata);\n\n if (validationResult instanceof Promise) {\n throw APIError.from('INTERNAL_SERVER_ERROR', BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED);\n }\n\n if (validationResult.issues) {\n logger.error('Invalid metadata', validationResult.issues);\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.INVALID_METADATA);\n }\n\n return validationResult.value as Record<string, any>;\n}\n","import { type BetterAuthPluginDBSchema } from 'better-auth';\nimport { mergeSchema } from 'better-auth/db';\n\nimport type { LeadOptions } from './type';\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 type { BetterAuthPlugin } from 'better-auth';\n\nimport { LEAD_ERROR_CODES } from './error-codes';\nimport { resend, subscribe, unsubscribe, update, verify } from './routes';\nimport { getSchema } from './schema';\nimport type { LeadOptions } from './type';\n\nexport const lead = <O extends LeadOptions>(options: O = {} as 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 update: update(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 $ERROR_CODES: LEAD_ERROR_CODES,\n } satisfies BetterAuthPlugin;\n};\n\nexport type * from './type';\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth-lead",
3
- "version": "0.0.1-dev.2",
3
+ "version": "0.1.0",
4
4
  "description": "Better Auth Lead plugin",
5
5
  "homepage": "https://github.com/marcjulian/better-auth-plugins#readme",
6
6
  "bugs": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "jose": "^6.1.3",
28
+ "jose": "^6.2.1",
29
29
  "zod": "^4.3.6"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/node": "^25.3.0",
32
+ "@types/node": "^25.3.5",
33
33
  "bumpp": "^10.4.1",
34
- "tsdown": "^0.20.3",
34
+ "tsdown": "^0.21.1",
35
35
  "typescript": "^5.9.3",
36
36
  "vitest": "^4.0.18"
37
37
  },
38
38
  "peerDependencies": {
39
- "better-auth": "^1.5.2"
39
+ "better-auth": "^1.5.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsdown",
@@ -1,98 +0,0 @@
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-KTJzD7Eo.d.mts.map