better-auth-lead 0.0.1-dev.2 → 0.2.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,16 @@ 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
+ - `createdAt`: The timestamp when the lead was created.
117
+ - `isNewLead`: whether the lead was newly created or already existed - useful to bounce verification emails together with `createdAt`
118
+
119
+ and a `request` object as the second parameter.
120
+
102
121
  ```ts
103
122
  // server/auth.ts
104
123
  import { betterAuth } from 'better-auth';
@@ -115,6 +134,10 @@ export const auth = betterAuth({
115
134
  text: `Click the link to verify your email: ${url}`,
116
135
  });
117
136
  },
137
+ onEmailVerified: async ({ lead }) => {
138
+ // do something when a lead's email is verified
139
+ console.log(`Lead ${lead.email} has been verified!`);
140
+ },
118
141
  }),
119
142
  ],
120
143
  });
@@ -122,6 +145,35 @@ export const auth = betterAuth({
122
145
 
123
146
  > Avoid awaiting the email sending to prevent timing attacks.
124
147
 
148
+ Additionally, you can provide an `onEmailVerified` callback to execute logic after a lead's email is verified.
149
+
150
+ ### Metadata Validation
151
+
152
+ To validate and parse metadata, you can pass a Standard Schema compatible schema (e.g. Zod, Valibot, ArkType).
153
+
154
+ ```ts
155
+ // server/auth.ts
156
+ import { betterAuth } from 'better-auth';
157
+ import { lead } from 'better-auth-lead';
158
+ import * as z from 'zod';
159
+
160
+ const metadataSchema = z.object({
161
+ preferences: z.enum(['engineering', 'marketing', 'design']),
162
+ });
163
+
164
+ export const auth = betterAuth({
165
+ plugins: [
166
+ lead({
167
+ metadata: {
168
+ validationSchema: metadataSchema,
169
+ },
170
+ }),
171
+ ],
172
+ });
173
+ ```
174
+
175
+ If the schema validation fails, the API `subscribe` and `update` routes will return a `400 Bad Request` error with `INVALID_METADATA`.
176
+
125
177
  ## Schema
126
178
 
127
179
  ### 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-CGt_sc05.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,210 @@
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
+ * @param createdAt the date the lead was created
54
+ * @param isNewLead whether the lead was newly created or already existed - useful to bounce verification emails together with `createdAt`
55
+ */
56
+
57
+ data: {
58
+ email: string;
59
+ url: string;
60
+ token: string;
61
+ createdAt: Date;
62
+ isNewLead: boolean;
63
+ }, request?: Request) => Promise<void>;
64
+ onEmailVerified?: (
65
+ /**
66
+ * @param lead the lead that was verified
67
+ */
68
+
69
+ data: {
70
+ lead: Lead;
71
+ }, request?: Request) => Promise<void>;
72
+ /**
73
+ * Number of seconds the verification token is
74
+ * valid for.
75
+ * @default 3600 seconds (1 hour)
76
+ */
77
+ expiresIn?: number;
78
+ /**
79
+ * Rate limit configuration for /lead/subscribe and /lead/resend endpoints.
80
+ */
81
+ rateLimit?: {
82
+ /**
83
+ * Time window in seconds for which the rate limit applies.
84
+ * @default 10 seconds
85
+ */
86
+ window: number;
87
+ /**
88
+ * Maximum number of requests allowed within the time window.
89
+ * @default 3 requests
90
+ */
91
+ max: number;
92
+ };
93
+ /**
94
+ * Schema for the lead plugin
95
+ */
96
+ schema?: InferOptionSchema<typeof lead$1> | undefined;
97
+ metadata?: {
98
+ validationSchema?: StandardSchemaV1;
99
+ };
100
+ }
101
+ interface Lead {
102
+ /**
103
+ * Database identifier
104
+ */
105
+ id: string;
106
+ createdAt: Date;
107
+ updatedAt: Date;
108
+ email: string;
109
+ emailVerified: boolean;
110
+ metadata?: string;
111
+ }
112
+ type LeadPayload = Omit<Lead, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified'>;
113
+ //#endregion
114
+ //#region src/index.d.ts
115
+ declare const lead: <O extends LeadOptions>(options?: O) => {
116
+ id: "lead";
117
+ schema: {
118
+ lead: {
119
+ fields: {
120
+ createdAt: {
121
+ type: "date";
122
+ defaultValue: () => Date;
123
+ required: true;
124
+ input: false;
125
+ };
126
+ updatedAt: {
127
+ type: "date";
128
+ defaultValue: () => Date;
129
+ onUpdate: () => Date;
130
+ required: true;
131
+ input: false;
132
+ };
133
+ email: {
134
+ type: "string";
135
+ required: true;
136
+ unique: true;
137
+ };
138
+ emailVerified: {
139
+ type: "boolean";
140
+ defaultValue: false;
141
+ required: true;
142
+ input: false;
143
+ };
144
+ metadata: {
145
+ type: "string";
146
+ required: false;
147
+ };
148
+ };
149
+ };
150
+ };
151
+ endpoints: {
152
+ subscribe: better_auth0.StrictEndpoint<"/lead/subscribe", {
153
+ method: "POST";
154
+ body: zod.ZodObject<{
155
+ email: zod.ZodString;
156
+ metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
157
+ }, better_auth0.$strip>;
158
+ }, {
159
+ status: boolean;
160
+ }>;
161
+ verify: better_auth0.StrictEndpoint<"/lead/verify", {
162
+ method: "GET";
163
+ query: zod.ZodObject<{
164
+ token: zod.ZodString;
165
+ }, better_auth0.$strip>;
166
+ }, {
167
+ status: boolean;
168
+ }>;
169
+ unsubscribe: better_auth0.StrictEndpoint<"/lead/unsubscribe", {
170
+ method: "POST";
171
+ body: zod.ZodObject<{
172
+ id: zod.ZodString;
173
+ }, better_auth0.$strip>;
174
+ }, {
175
+ status: boolean;
176
+ }>;
177
+ resend: better_auth0.StrictEndpoint<"/lead/resend", {
178
+ method: "POST";
179
+ body: zod.ZodObject<{
180
+ email: zod.ZodString;
181
+ }, better_auth0.$strip>;
182
+ }, {
183
+ status: boolean;
184
+ }>;
185
+ update: better_auth0.StrictEndpoint<"/lead/update", {
186
+ method: "POST";
187
+ body: zod.ZodObject<{
188
+ id: zod.ZodString;
189
+ metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
190
+ }, better_auth0.$strip>;
191
+ }, {
192
+ status: boolean;
193
+ }>;
194
+ };
195
+ options: NoInfer<O>;
196
+ rateLimit: {
197
+ pathMatcher: (path: string) => boolean;
198
+ window: number;
199
+ max: number;
200
+ }[];
201
+ $ERROR_CODES: {
202
+ INVALID_EMAIL: better_auth0.RawError<"INVALID_EMAIL">;
203
+ INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
204
+ TOKEN_EXPIRED: better_auth0.RawError<"TOKEN_EXPIRED">;
205
+ INVALID_METADATA: better_auth0.RawError<"INVALID_METADATA">;
206
+ };
207
+ };
208
+ //#endregion
209
+ export { LeadPayload as i, Lead as n, LeadOptions as r, lead as t };
210
+ //# sourceMappingURL=index-CGt_sc05.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-CGt_sc05.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",
@@ -25,6 +25,7 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
25
25
  value: normalizedEmail
26
26
  }]
27
27
  });
28
+ let isNewLead = false;
28
29
  if (!lead) try {
29
30
  lead = await ctx.context.adapter.create({
30
31
  model: "lead",
@@ -33,6 +34,7 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
33
34
  metadata: metadata ? JSON.stringify(metadata) : void 0
34
35
  }
35
36
  });
37
+ isNewLead = true;
36
38
  } catch (e) {
37
39
  ctx.context.logger.info("Error creating lead");
38
40
  lead = await ctx.context.adapter.findOne({
@@ -49,7 +51,9 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
49
51
  await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
50
52
  email: normalizedEmail,
51
53
  url,
52
- token
54
+ token,
55
+ createdAt: lead.createdAt,
56
+ isNewLead
53
57
  }, ctx.request));
54
58
  }
55
59
  return ctx.json({ status: true });
@@ -68,7 +72,7 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
68
72
  throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.INVALID_TOKEN);
69
73
  }
70
74
  const parsed = subscribeSchema.parse(jwt.payload);
71
- const lead = await ctx.context.adapter.findOne({
75
+ let lead = await ctx.context.adapter.findOne({
72
76
  model: "lead",
73
77
  where: [{
74
78
  field: "email",
@@ -77,7 +81,7 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
77
81
  });
78
82
  if (!lead) return ctx.json({ status: true });
79
83
  if (lead.emailVerified) return ctx.json({ status: true });
80
- await ctx.context.adapter.update({
84
+ lead = await ctx.context.adapter.update({
81
85
  model: "lead",
82
86
  where: [{
83
87
  field: "email",
@@ -85,6 +89,8 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
85
89
  }],
86
90
  update: { emailVerified: true }
87
91
  });
92
+ if (!lead) return ctx.json({ status: true });
93
+ if (options.onEmailVerified) await ctx.context.runInBackgroundOrAwait(options.onEmailVerified({ lead }, ctx.request));
88
94
  return ctx.json({ status: true });
89
95
  });
90
96
  const unsubscribeSchema = z.object({ id: z.string().meta({ description: "The id of the lead to unsubscribe" }) });
@@ -131,7 +137,9 @@ const resend = (options) => createAuthEndpoint("/lead/resend", {
131
137
  await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
132
138
  email: normalizedEmail,
133
139
  url,
134
- token
140
+ token,
141
+ createdAt: lead.createdAt,
142
+ isNewLead: false
135
143
  }, ctx.request));
136
144
  }
137
145
  return ctx.json({ status: true });
@@ -144,7 +152,7 @@ const update = (options) => createAuthEndpoint("/lead/update", {
144
152
  method: "POST",
145
153
  body: updateSchema
146
154
  }, async (ctx) => {
147
- const { id, metadata } = ctx.body;
155
+ const { id } = ctx.body;
148
156
  if (!await ctx.context.adapter.findOne({
149
157
  model: "lead",
150
158
  where: [{
@@ -152,6 +160,7 @@ const update = (options) => createAuthEndpoint("/lead/update", {
152
160
  value: id
153
161
  }]
154
162
  })) return ctx.json({ status: true });
163
+ const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
155
164
  await ctx.context.adapter.update({
156
165
  model: "lead",
157
166
  where: [{
@@ -162,7 +171,16 @@ const update = (options) => createAuthEndpoint("/lead/update", {
162
171
  });
163
172
  return ctx.json({ status: true });
164
173
  });
165
-
174
+ function validateMetadata(options, metadata, logger) {
175
+ if (!metadata || !options.metadata?.validationSchema) return metadata;
176
+ const validationResult = options.metadata.validationSchema["~standard"].validate(metadata);
177
+ if (validationResult instanceof Promise) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED);
178
+ if (validationResult.issues) {
179
+ logger.error("Invalid metadata", validationResult.issues);
180
+ throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_METADATA);
181
+ }
182
+ return validationResult.value;
183
+ }
166
184
  //#endregion
167
185
  //#region src/schema.ts
168
186
  const lead$1 = { lead: { fields: {
@@ -198,7 +216,6 @@ const lead$1 = { lead: { fields: {
198
216
  const getSchema = (options) => {
199
217
  return mergeSchema(lead$1, options.schema);
200
218
  };
201
-
202
219
  //#endregion
203
220
  //#region src/index.ts
204
221
  const lead = (options = {}) => {
@@ -221,7 +238,7 @@ const lead = (options = {}) => {
221
238
  $ERROR_CODES: LEAD_ERROR_CODES
222
239
  };
223
240
  };
224
-
225
241
  //#endregion
226
242
  export { lead };
243
+
227
244
  //# 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 let isNewLead = false;\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 isNewLead = true;\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(\n { email: normalizedEmail, url, token, createdAt: lead.createdAt, isNewLead },\n ctx.request,\n ),\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(\n { email: normalizedEmail, url, token, createdAt: lead.createdAt, isNewLead: false },\n ctx.request,\n ),\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.2.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