better-auth-lead 0.0.1-dev.1 → 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,9 +11,11 @@ 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
- ## Usage
18
+ Add the plugin to your auth config
17
19
 
18
20
  ```ts
19
21
  // server/auth.ts
@@ -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:
@@ -42,3 +51,154 @@ const authClient = createAuthClient({
42
51
  plugins: [leadClient()],
43
52
  });
44
53
  ```
54
+
55
+ ## Usage
56
+
57
+ ### Subscribe
58
+
59
+ ```ts
60
+ // POST /lead/subscribe
61
+ const { data, error } = await authClient.lead.subscribe({
62
+ email: 'user@example.com',
63
+ // json object
64
+ metadata: {
65
+ preferences: 'engineering',
66
+ },
67
+ });
68
+ ```
69
+
70
+ ### Verify
71
+
72
+ ```ts
73
+ // GET /lead/verify
74
+ await authClient.lead.verify({
75
+ query: {
76
+ token,
77
+ },
78
+ });
79
+ ```
80
+
81
+ ### Unsubscribe
82
+
83
+ ```ts
84
+ // POST /lead/unsubscribe
85
+ const { data, error } = await authClient.lead.unsubscribe({ id: 'lead-id' });
86
+ ```
87
+
88
+ ### Resend
89
+
90
+ ```ts
91
+ // POST /lead/resend
92
+ const { data, error } = await authClient.lead.resend({
93
+ email: 'user@example.com',
94
+ });
95
+ ```
96
+
97
+ ### Update
98
+
99
+ ```ts
100
+ // POST /lead/update
101
+ const { data, error } = await authClient.lead.update({
102
+ id: 'lead-id',
103
+ metadata: {
104
+ preferences: 'ai',
105
+ },
106
+ });
107
+ ```
108
+
109
+ ### Email Verification
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
+
119
+ ```ts
120
+ // server/auth.ts
121
+ import { betterAuth } from 'better-auth';
122
+ import { lead } from 'better-auth-lead';
123
+ import { sendEmail } from './email'; // your email sending function
124
+
125
+ export const auth = betterAuth({
126
+ plugins: [
127
+ lead({
128
+ sendVerificationEmail: async ({ email, url, token }) => {
129
+ void sendEmail({
130
+ to: email,
131
+ subject: 'Newsletter: Verify your email address',
132
+ text: `Click the link to verify your email: ${url}`,
133
+ });
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
+ },
139
+ }),
140
+ ],
141
+ });
142
+ ```
143
+
144
+ > Avoid awaiting the email sending to prevent timing attacks.
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
+
175
+ ## Schema
176
+
177
+ ### Lead
178
+
179
+ Table name: `lead`
180
+
181
+ |  Field |  Type |  Key |  Description |
182
+ | -------------- | ------- | ------ | ------------------------------- |
183
+ | id | string | pk | Unique identifier for each lead |
184
+ | email | string | unique | Email address of the lead |
185
+ |  emailVerified | boolean | | Whether the email is verified |
186
+ | metadata | json | ? | Additional data about the lead |
187
+ | createdAt | date | | Timestamp of lead creation |
188
+ | updatedAt | date | | Timestamp of last update |
189
+
190
+ #### Prisma
191
+
192
+ ```prisma
193
+ model Lead {
194
+ id String @id
195
+ createdAt DateTime @default(now())
196
+ updatedAt DateTime @updatedAt
197
+ email String
198
+ emailVerified Boolean @default(false)
199
+ metadata String?
200
+
201
+ @@unique([email])
202
+ @@map("lead")
203
+ }
204
+ ```
package/dist/client.d.mts CHANGED
@@ -1,10 +1,16 @@
1
- import "./type-CedeOvZ6.mjs";
2
- import { lead } from "./index.mjs";
1
+ import { t as lead } from "./index-B-BhgW_e.mjs";
2
+ import * as better_auth0 from "better-auth";
3
3
 
4
4
  //#region src/client.d.ts
5
5
  declare const leadClient: () => {
6
6
  id: "lead";
7
7
  $InferServerPlugin: ReturnType<typeof lead>;
8
+ $ERROR_CODES: {
9
+ INVALID_EMAIL: better_auth0.RawError<"INVALID_EMAIL">;
10
+ INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
11
+ TOKEN_EXPIRED: better_auth0.RawError<"TOKEN_EXPIRED">;
12
+ INVALID_METADATA: better_auth0.RawError<"INVALID_METADATA">;
13
+ };
8
14
  };
9
15
  //#endregion
10
16
  export { leadClient };
package/dist/client.mjs CHANGED
@@ -1,11 +1,13 @@
1
+ import { t as LEAD_ERROR_CODES } from "./error-codes-CZUPEOrE.mjs";
1
2
  //#region src/client.ts
2
3
  const leadClient = () => {
3
4
  return {
4
5
  id: "lead",
5
- $InferServerPlugin: {}
6
+ $InferServerPlugin: {},
7
+ $ERROR_CODES: LEAD_ERROR_CODES
6
8
  };
7
9
  };
8
-
9
10
  //#endregion
10
11
  export { leadClient };
12
+
11
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';\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"}
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"}
@@ -0,0 +1,12 @@
1
+ import { defineErrorCodes } from "better-auth";
2
+ //#region src/error-codes.ts
3
+ const LEAD_ERROR_CODES = defineErrorCodes({
4
+ INVALID_EMAIL: "Invalid email",
5
+ INVALID_TOKEN: "Invalid token",
6
+ TOKEN_EXPIRED: "Token expired",
7
+ INVALID_METADATA: "Invalid metadata"
8
+ });
9
+ //#endregion
10
+ export { LEAD_ERROR_CODES as t };
11
+
12
+ //# sourceMappingURL=error-codes-CZUPEOrE.mjs.map
@@ -0,0 +1 @@
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,90 +1,2 @@
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
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,57 +1,22 @@
1
- import "better-auth";
2
- import { mergeSchema } from "better-auth/db";
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
- import * as z from "zod";
5
4
  import { jwtVerify } from "jose";
6
5
  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
6
+ import * as z from "zod";
7
+ import { mergeSchema } from "better-auth/db";
44
8
  //#region src/routes.ts
45
9
  const subscribeSchema = z.object({
46
- email: z.email(),
47
- metadata: z.record(z.string(), z.any()).optional()
10
+ email: z.string().meta({ description: "Email address of the lead" }),
11
+ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" })
48
12
  });
49
13
  const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
50
14
  method: "POST",
51
- body: subscribeSchema,
52
- metadata: {}
15
+ body: subscribeSchema
53
16
  }, async (ctx) => {
54
- const { email, metadata } = ctx.body;
17
+ const { email } = ctx.body;
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);
55
20
  const normalizedEmail = email.toLowerCase();
56
21
  let lead = await ctx.context.adapter.findOne({
57
22
  model: "lead",
@@ -78,14 +43,6 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
78
43
  }]
79
44
  });
80
45
  }
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
46
  if (options.sendVerificationEmail && lead && !lead.emailVerified) {
90
47
  const token = await createEmailVerificationToken(ctx.context.secret, normalizedEmail, void 0, options.expiresIn ?? 3600);
91
48
  const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
@@ -97,22 +54,21 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
97
54
  }
98
55
  return ctx.json({ status: true });
99
56
  });
100
- const verifySchema = z.object({ token: z.string() });
57
+ const verifySchema = z.object({ token: z.string().meta({ description: "The token to verify the email" }) });
101
58
  const verify = (options) => createAuthEndpoint("/lead/verify", {
102
59
  method: "GET",
103
- query: verifySchema,
104
- metadata: {}
60
+ query: verifySchema
105
61
  }, async (ctx) => {
106
62
  const { token } = ctx.query;
107
63
  let jwt;
108
64
  try {
109
65
  jwt = await jwtVerify(token, new TextEncoder().encode(ctx.context.secret), { algorithms: ["HS256"] });
110
66
  } catch (e) {
111
- if (e instanceof JWTExpired) throw new APIError("UNAUTHORIZED", { message: "Token expired" });
112
- throw new APIError("UNAUTHORIZED", { message: "Invalid token" });
67
+ if (e instanceof JWTExpired) throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.TOKEN_EXPIRED);
68
+ throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.INVALID_TOKEN);
113
69
  }
114
70
  const parsed = subscribeSchema.parse(jwt.payload);
115
- const lead = await ctx.context.adapter.findOne({
71
+ let lead = await ctx.context.adapter.findOne({
116
72
  model: "lead",
117
73
  where: [{
118
74
  field: "email",
@@ -121,7 +77,7 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
121
77
  });
122
78
  if (!lead) return ctx.json({ status: true });
123
79
  if (lead.emailVerified) return ctx.json({ status: true });
124
- await ctx.context.adapter.update({
80
+ lead = await ctx.context.adapter.update({
125
81
  model: "lead",
126
82
  where: [{
127
83
  field: "email",
@@ -129,13 +85,14 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
129
85
  }],
130
86
  update: { emailVerified: true }
131
87
  });
88
+ if (!lead) return ctx.json({ status: true });
89
+ if (options.onEmailVerified) await ctx.context.runInBackgroundOrAwait(options.onEmailVerified({ lead }, ctx.request));
132
90
  return ctx.json({ status: true });
133
91
  });
134
- const unsubscribeSchema = z.object({ id: z.string() });
92
+ const unsubscribeSchema = z.object({ id: z.string().meta({ description: "The id of the lead to unsubscribe" }) });
135
93
  const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
136
94
  method: "POST",
137
- body: unsubscribeSchema,
138
- metadata: {}
95
+ body: unsubscribeSchema
139
96
  }, async (ctx) => {
140
97
  const { id } = ctx.body;
141
98
  if (!await ctx.context.adapter.findOne({
@@ -154,13 +111,13 @@ const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
154
111
  });
155
112
  return ctx.json({ status: true });
156
113
  });
157
- const resendSchema = z.object({ email: z.email() });
114
+ const resendSchema = z.object({ email: z.string().meta({ description: "Email address to resend the verification email to" }) });
158
115
  const resend = (options) => createAuthEndpoint("/lead/resend", {
159
116
  method: "POST",
160
- body: resendSchema,
161
- metadata: {}
117
+ body: resendSchema
162
118
  }, async (ctx) => {
163
119
  const { email } = ctx.body;
120
+ if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
164
121
  const normalizedEmail = email.toLowerCase();
165
122
  const lead = await ctx.context.adapter.findOne({
166
123
  model: "lead",
@@ -181,10 +138,81 @@ const resend = (options) => createAuthEndpoint("/lead/resend", {
181
138
  }
182
139
  return ctx.json({ status: true });
183
140
  });
184
-
141
+ const updateSchema = z.object({
142
+ id: z.string().meta({ description: "The id of the lead to update" }),
143
+ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" })
144
+ });
145
+ const update = (options) => createAuthEndpoint("/lead/update", {
146
+ method: "POST",
147
+ body: updateSchema
148
+ }, async (ctx) => {
149
+ const { id } = ctx.body;
150
+ if (!await ctx.context.adapter.findOne({
151
+ model: "lead",
152
+ where: [{
153
+ field: "id",
154
+ value: id
155
+ }]
156
+ })) return ctx.json({ status: true });
157
+ const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
158
+ await ctx.context.adapter.update({
159
+ model: "lead",
160
+ where: [{
161
+ field: "id",
162
+ value: id
163
+ }],
164
+ update: { metadata: metadata ? JSON.stringify(metadata) : void 0 }
165
+ });
166
+ return ctx.json({ status: true });
167
+ });
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
+ }
178
+ //#endregion
179
+ //#region src/schema.ts
180
+ const lead$1 = { lead: { fields: {
181
+ createdAt: {
182
+ type: "date",
183
+ defaultValue: () => /* @__PURE__ */ new Date(),
184
+ required: true,
185
+ input: false
186
+ },
187
+ updatedAt: {
188
+ type: "date",
189
+ defaultValue: () => /* @__PURE__ */ new Date(),
190
+ onUpdate: () => /* @__PURE__ */ new Date(),
191
+ required: true,
192
+ input: false
193
+ },
194
+ email: {
195
+ type: "string",
196
+ required: true,
197
+ unique: true
198
+ },
199
+ emailVerified: {
200
+ type: "boolean",
201
+ defaultValue: false,
202
+ required: true,
203
+ input: false
204
+ },
205
+ metadata: {
206
+ type: "string",
207
+ required: false
208
+ }
209
+ } } };
210
+ const getSchema = (options) => {
211
+ return mergeSchema(lead$1, options.schema);
212
+ };
185
213
  //#endregion
186
214
  //#region src/index.ts
187
- const lead = (options) => {
215
+ const lead = (options = {}) => {
188
216
  return {
189
217
  id: "lead",
190
218
  schema: getSchema(options),
@@ -192,17 +220,19 @@ const lead = (options) => {
192
220
  subscribe: subscribe(options),
193
221
  verify: verify(options),
194
222
  unsubscribe: unsubscribe(options),
195
- resend: resend(options)
223
+ resend: resend(options),
224
+ update: update(options)
196
225
  },
197
226
  options,
198
227
  rateLimit: [{
199
228
  pathMatcher: (path) => ["/lead/subscribe", "/lead/resend"].includes(path),
200
229
  window: options.rateLimit?.window ?? 10,
201
230
  max: options.rateLimit?.max ?? 3
202
- }]
231
+ }],
232
+ $ERROR_CODES: LEAD_ERROR_CODES
203
233
  };
204
234
  };
205
-
206
235
  //#endregion
207
236
  export { lead };
237
+
208
238
  //# sourceMappingURL=index.mjs.map
@@ -1 +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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"}
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.1",
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.1"
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-CedeOvZ6.d.mts.map