better-auth-lead 0.2.0 → 0.4.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
@@ -110,11 +110,9 @@ const { data, error } = await authClient.lead.update({
110
110
 
111
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
112
 
113
- - `email`: The lead email.
113
+ - `lead`: The lead object.
114
114
  - `url`: The URL to send to the user which contains the token.
115
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
116
 
119
117
  and a `request` object as the second parameter.
120
118
 
@@ -127,12 +125,25 @@ import { sendEmail } from './email'; // your email sending function
127
125
  export const auth = betterAuth({
128
126
  plugins: [
129
127
  lead({
130
- sendVerificationEmail: async ({ email, url, token }) => {
128
+ sendVerificationEmail: async ({ lead, url, token }) => {
129
+ const { verificationEmailSentAt } = lead;
130
+ if (
131
+ verificationEmailSentAt &&
132
+ Date.now() - verificationEmailSentAt.getTime() < 60 * 1000 // 1 minute
133
+ ) {
134
+ console.log(
135
+ `Skipping sending verification email to ${lead.email} because a recent email was already sent.`,
136
+ );
137
+ return false;
138
+ }
139
+
131
140
  void sendEmail({
132
- to: email,
141
+ to: lead.email,
133
142
  subject: 'Newsletter: Verify your email address',
134
143
  text: `Click the link to verify your email: ${url}`,
135
144
  });
145
+
146
+ return true;
136
147
  },
137
148
  onEmailVerified: async ({ lead }) => {
138
149
  // do something when a lead's email is verified
@@ -149,30 +160,54 @@ Additionally, you can provide an `onEmailVerified` callback to execute logic aft
149
160
 
150
161
  ### Metadata Validation
151
162
 
152
- To validate and parse metadata, you can pass a Standard Schema compatible schema (e.g. Zod, Valibot, ArkType).
163
+ To validate and parse metadata, pass a Standard Schema compatible schema (e.g. Zod, Valibot, ArkType) to the `metadata.validationSchema` option. If validation fails, `subscribe` and `update` return a `400 Bad Request` with `INVALID_METADATA`.
164
+
165
+ To share the type with the client without bundling server code, define the schema in a shared file and import only the type on the client side:
153
166
 
154
167
  ```ts
155
- // server/auth.ts
156
- import { betterAuth } from 'better-auth';
157
- import { lead } from 'better-auth-lead';
168
+ // shared/lead-metadata-schema.ts
158
169
  import * as z from 'zod';
159
170
 
160
- const metadataSchema = z.object({
171
+ export const leadMetadataSchema = z.object({
161
172
  preferences: z.enum(['engineering', 'marketing', 'design']),
162
173
  });
163
174
 
175
+ export type LeadMetadata = z.infer<typeof leadMetadataSchema>;
176
+ ```
177
+
178
+ ```ts
179
+ // server/auth.ts
180
+ import { betterAuth } from 'better-auth';
181
+ import { lead } from 'better-auth-lead';
182
+ import { leadMetadataSchema } from './shared/lead-metadata-schema';
183
+
164
184
  export const auth = betterAuth({
165
185
  plugins: [
166
186
  lead({
167
187
  metadata: {
168
- validationSchema: metadataSchema,
188
+ validationSchema: leadMetadataSchema,
169
189
  },
170
190
  }),
171
191
  ],
172
192
  });
173
193
  ```
174
194
 
175
- If the schema validation fails, the API `subscribe` and `update` routes will return a `400 Bad Request` error with `INVALID_METADATA`.
195
+ ```ts
196
+ // client/auth-client.ts
197
+ import { createAuthClient } from 'better-auth/client';
198
+ import { leadClient } from 'better-auth-lead/client';
199
+ import type { LeadMetadata } from './shared/lead-metadata-schema';
200
+
201
+ const authClient = createAuthClient({
202
+ plugins: [leadClient<LeadMetadata>()],
203
+ });
204
+
205
+ // metadata is now typed as LeadMetadata
206
+ await authClient.lead.subscribe({
207
+ email: 'user@example.com',
208
+ metadata: { preferences: 'engineering' },
209
+ });
210
+ ```
176
211
 
177
212
  ## Schema
178
213
 
@@ -180,25 +215,27 @@ If the schema validation fails, the API `subscribe` and `update` routes will ret
180
215
 
181
216
  Table name: `lead`
182
217
 
183
- |  Field |  Type |  Key |  Description |
184
- | -------------- | ------- | ------ | ------------------------------- |
185
- | id | string | pk | Unique identifier for each lead |
186
- | email | string | unique | Email address of the lead |
187
- |  emailVerified | boolean | | Whether the email is verified |
188
- | metadata | json | ? | Additional data about the lead |
189
- | createdAt | date | | Timestamp of lead creation |
190
- | updatedAt | date | | Timestamp of last update |
218
+ |  Field |  Type |  Key |  Description |
219
+ | ----------------------- | ------- | ------ | ------------------------------------------------- |
220
+ | id | string | pk | Unique identifier for each lead |
221
+ | email | string | unique | Email address of the lead |
222
+ | emailVerified | boolean | | Whether the email is verified |
223
+ | verificationEmailSentAt | Date | ? | Timestamp of when the verification email was sent |
224
+ | metadata | json | ? | Additional data about the lead |
225
+ | createdAt | date | | Timestamp of lead creation |
226
+ | updatedAt | date | | Timestamp of last update |
191
227
 
192
228
  #### Prisma
193
229
 
194
230
  ```prisma
195
231
  model Lead {
196
- id String @id
197
- createdAt DateTime @default(now())
198
- updatedAt DateTime @updatedAt
199
- email String
200
- emailVerified Boolean @default(false)
201
- metadata String?
232
+ id String @id
233
+ createdAt DateTime @default(now())
234
+ updatedAt DateTime @updatedAt
235
+ email String
236
+ emailVerified Boolean @default(false)
237
+ verificationEmailSentAt DateTime?
238
+ metadata String?
202
239
 
203
240
  @@unique([email])
204
241
  @@map("lead")
package/dist/client.d.mts CHANGED
@@ -1,16 +1,14 @@
1
- import { t as lead } from "./index-CGt_sc05.mjs";
2
- import * as better_auth0 from "better-auth";
1
+ import { r as LeadOptions, t as lead } from "./index-E9vItimA.mjs";
2
+ import { StandardSchemaV1 } from "better-auth";
3
3
 
4
4
  //#region src/client.d.ts
5
- declare const leadClient: () => {
5
+ declare const leadClient: <TMetadata = undefined>() => {
6
6
  id: "lead";
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
- };
7
+ $InferServerPlugin: ReturnType<typeof lead<[TMetadata] extends [undefined] ? LeadOptions : LeadOptions & {
8
+ metadata: {
9
+ validationSchema: StandardSchemaV1<unknown, TMetadata>;
10
+ };
11
+ }>>;
14
12
  };
15
13
  //#endregion
16
14
  export { leadClient };
package/dist/client.mjs CHANGED
@@ -1,10 +1,8 @@
1
- import { t as LEAD_ERROR_CODES } from "./error-codes-CZUPEOrE.mjs";
2
1
  //#region src/client.ts
3
2
  const leadClient = () => {
4
3
  return {
5
4
  id: "lead",
6
- $InferServerPlugin: {},
7
- $ERROR_CODES: LEAD_ERROR_CODES
5
+ $InferServerPlugin: {}
8
6
  };
9
7
  };
10
8
  //#endregion
@@ -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 { StandardSchemaV1 } from 'better-auth';\nimport type { BetterAuthClientPlugin } from 'better-auth/client';\n\nimport type { lead } from './index';\nimport type { LeadOptions } from './type';\n\nexport const leadClient = <TMetadata = undefined>() => {\n type O = [TMetadata] extends [undefined]\n ? LeadOptions\n : LeadOptions & { metadata: { validationSchema: StandardSchemaV1<unknown, TMetadata> } };\n return {\n id: 'lead',\n $InferServerPlugin: {} as ReturnType<typeof lead<O>>,\n } satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";AAMA,MAAa,mBAA0C;AAIrD,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EACvB"}
@@ -30,6 +30,11 @@ declare const lead$1: {
30
30
  required: true;
31
31
  input: false;
32
32
  };
33
+ verificationEmailSentAt: {
34
+ type: "date";
35
+ required: false;
36
+ input: false;
37
+ };
33
38
  metadata: {
34
39
  type: "string";
35
40
  required: false;
@@ -47,20 +52,16 @@ interface LeadOptions {
47
52
  */
48
53
  sendVerificationEmail?: (
49
54
  /**
50
- * @param email the email to send the verification email to
55
+ * @param lead the lead to send the verification email to
51
56
  * @param url the verification url
52
57
  * @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
58
  */
56
59
 
57
60
  data: {
58
- email: string;
61
+ lead: Lead;
59
62
  url: string;
60
63
  token: string;
61
- createdAt: Date;
62
- isNewLead: boolean;
63
- }, request?: Request) => Promise<void>;
64
+ }, request?: Request) => Promise<boolean>;
64
65
  onEmailVerified?: (
65
66
  /**
66
67
  * @param lead the lead that was verified
@@ -107,9 +108,10 @@ interface Lead {
107
108
  updatedAt: Date;
108
109
  email: string;
109
110
  emailVerified: boolean;
111
+ verificationEmailSentAt: Date | null;
110
112
  metadata?: string;
111
113
  }
112
- type LeadPayload = Omit<Lead, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified'>;
114
+ type LeadPayload = Omit<Lead, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified' | 'verificationEmailSentAt'>;
113
115
  //#endregion
114
116
  //#region src/index.d.ts
115
117
  declare const lead: <O extends LeadOptions>(options?: O) => {
@@ -141,6 +143,11 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
141
143
  required: true;
142
144
  input: false;
143
145
  };
146
+ verificationEmailSentAt: {
147
+ type: "date";
148
+ required: false;
149
+ input: false;
150
+ };
144
151
  metadata: {
145
152
  type: "string";
146
153
  required: false;
@@ -155,6 +162,18 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
155
162
  email: zod.ZodString;
156
163
  metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
157
164
  }, better_auth0.$strip>;
165
+ metadata: {
166
+ $Infer: {
167
+ body: {
168
+ email: string;
169
+ metadata?: (O extends {
170
+ metadata: {
171
+ validationSchema: better_auth0.StandardSchemaV1<unknown, infer Out>;
172
+ };
173
+ } ? Out : Record<string, any>) | undefined;
174
+ };
175
+ };
176
+ };
158
177
  }, {
159
178
  status: boolean;
160
179
  }>;
@@ -188,6 +207,18 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
188
207
  id: zod.ZodString;
189
208
  metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
190
209
  }, better_auth0.$strip>;
210
+ metadata: {
211
+ $Infer: {
212
+ body: {
213
+ id: string;
214
+ metadata?: (O extends {
215
+ metadata: {
216
+ validationSchema: better_auth0.StandardSchemaV1<unknown, infer Out>;
217
+ };
218
+ } ? Out : Record<string, any>) | undefined;
219
+ };
220
+ };
221
+ };
191
222
  }, {
192
223
  status: boolean;
193
224
  }>;
@@ -207,4 +238,4 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
207
238
  };
208
239
  //#endregion
209
240
  export { LeadPayload as i, Lead as n, LeadOptions as r, lead as t };
210
- //# sourceMappingURL=index-CGt_sc05.d.mts.map
241
+ //# sourceMappingURL=index-E9vItimA.d.mts.map
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as LeadPayload, n as Lead, r as LeadOptions, t as lead } from "./index-CGt_sc05.mjs";
1
+ import { i as LeadPayload, n as Lead, r as LeadOptions, t as lead } from "./index-E9vItimA.mjs";
2
2
  export { Lead, LeadOptions, LeadPayload, lead };
package/dist/index.mjs CHANGED
@@ -1,10 +1,17 @@
1
- import { t as LEAD_ERROR_CODES } from "./error-codes-CZUPEOrE.mjs";
2
- import { BASE_ERROR_CODES } from "better-auth";
1
+ import { BASE_ERROR_CODES, defineErrorCodes } from "better-auth";
3
2
  import { APIError, createAuthEndpoint, createEmailVerificationToken } from "better-auth/api";
4
3
  import { jwtVerify } from "jose";
5
4
  import { JWTExpired } from "jose/errors";
6
5
  import * as z from "zod";
7
6
  import { mergeSchema } from "better-auth/db";
7
+ //#region src/error-codes.ts
8
+ const LEAD_ERROR_CODES = defineErrorCodes({
9
+ INVALID_EMAIL: "Invalid email",
10
+ INVALID_TOKEN: "Invalid token",
11
+ TOKEN_EXPIRED: "Token expired",
12
+ INVALID_METADATA: "Invalid metadata"
13
+ });
14
+ //#endregion
8
15
  //#region src/routes.ts
9
16
  const subscribeSchema = z.object({
10
17
  email: z.string().meta({ description: "Email address of the lead" }),
@@ -12,7 +19,8 @@ const subscribeSchema = z.object({
12
19
  });
13
20
  const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
14
21
  method: "POST",
15
- body: subscribeSchema
22
+ body: subscribeSchema,
23
+ metadata: { $Infer: { body: {} } }
16
24
  }, async (ctx) => {
17
25
  const { email } = ctx.body;
18
26
  if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
@@ -25,7 +33,6 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
25
33
  value: normalizedEmail
26
34
  }]
27
35
  });
28
- let isNewLead = false;
29
36
  if (!lead) try {
30
37
  lead = await ctx.context.adapter.create({
31
38
  model: "lead",
@@ -34,7 +41,6 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
34
41
  metadata: metadata ? JSON.stringify(metadata) : void 0
35
42
  }
36
43
  });
37
- isNewLead = true;
38
44
  } catch (e) {
39
45
  ctx.context.logger.info("Error creating lead");
40
46
  lead = await ctx.context.adapter.findOne({
@@ -48,13 +54,18 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
48
54
  if (options.sendVerificationEmail && lead && !lead.emailVerified) {
49
55
  const token = await createEmailVerificationToken(ctx.context.secret, normalizedEmail, void 0, options.expiresIn ?? 3600);
50
56
  const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
51
- await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
52
- email: normalizedEmail,
57
+ if (await options.sendVerificationEmail({
58
+ lead,
53
59
  url,
54
- token,
55
- createdAt: lead.createdAt,
56
- isNewLead
57
- }, ctx.request));
60
+ token
61
+ }, ctx.request)) await ctx.context.adapter.update({
62
+ model: "lead",
63
+ where: [{
64
+ field: "email",
65
+ value: normalizedEmail
66
+ }],
67
+ update: { verificationEmailSentAt: /* @__PURE__ */ new Date() }
68
+ });
58
69
  }
59
70
  return ctx.json({ status: true });
60
71
  });
@@ -134,13 +145,18 @@ const resend = (options) => createAuthEndpoint("/lead/resend", {
134
145
  if (options.sendVerificationEmail && lead && !lead.emailVerified) {
135
146
  const token = await createEmailVerificationToken(ctx.context.secret, normalizedEmail, void 0, options.expiresIn ?? 3600);
136
147
  const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
137
- await ctx.context.runInBackgroundOrAwait(options.sendVerificationEmail({
138
- email: normalizedEmail,
148
+ if (await options.sendVerificationEmail({
149
+ lead,
139
150
  url,
140
- token,
141
- createdAt: lead.createdAt,
142
- isNewLead: false
143
- }, ctx.request));
151
+ token
152
+ }, ctx.request)) await ctx.context.adapter.update({
153
+ model: "lead",
154
+ where: [{
155
+ field: "email",
156
+ value: normalizedEmail
157
+ }],
158
+ update: { verificationEmailSentAt: /* @__PURE__ */ new Date() }
159
+ });
144
160
  }
145
161
  return ctx.json({ status: true });
146
162
  });
@@ -150,7 +166,8 @@ const updateSchema = z.object({
150
166
  });
151
167
  const update = (options) => createAuthEndpoint("/lead/update", {
152
168
  method: "POST",
153
- body: updateSchema
169
+ body: updateSchema,
170
+ metadata: { $Infer: { body: {} } }
154
171
  }, async (ctx) => {
155
172
  const { id } = ctx.body;
156
173
  if (!await ctx.context.adapter.findOne({
@@ -208,6 +225,11 @@ const lead$1 = { lead: { fields: {
208
225
  required: true,
209
226
  input: false
210
227
  },
228
+ verificationEmailSentAt: {
229
+ type: "date",
230
+ required: false,
231
+ input: false
232
+ },
211
233
  metadata: {
212
234
  type: "string",
213
235
  required: false
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.mjs","names":["lead"],"sources":["../src/error-codes.ts","../src/routes.ts","../src/schema.ts","../src/index.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","import { BASE_ERROR_CODES, type InternalLogger, type StandardSchemaV1 } 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\ntype InferMetadata<O extends LeadOptions> = O extends {\n metadata: { validationSchema: StandardSchemaV1<unknown, infer Out> };\n}\n ? Out\n : Record<string, any>;\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 metadata: {\n $Infer: {\n body: {} as {\n email: string;\n metadata?: InferMetadata<O>;\n },\n },\n },\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(\n options,\n ctx.body.metadata as Record<string, any> | undefined,\n ctx.context.logger,\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 const sent = await options.sendVerificationEmail(\n {\n lead,\n url,\n token,\n },\n ctx.request,\n );\n\n if (sent) {\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [{ field: 'email', value: normalizedEmail }],\n update: { verificationEmailSentAt: new Date() },\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 const sent = await options.sendVerificationEmail(\n {\n lead,\n url,\n token,\n },\n ctx.request,\n );\n\n if (sent) {\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [{ field: 'email', value: normalizedEmail }],\n update: { verificationEmailSentAt: new Date() },\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 metadata: {\n $Infer: {\n body: {} as {\n id: string;\n metadata?: InferMetadata<O>;\n },\n },\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 const metadata = validateMetadata(\n options,\n ctx.body.metadata as Record<string, any> | undefined,\n ctx.context.logger,\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\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 verificationEmailSentAt: {\n type: 'date',\n required: false,\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":";;;;;;;AAEA,MAAa,mBAAmB,iBAAiB;CAC/C,eAAe;CACf,eAAe;CACf,eAAe;CACf,kBAAkB;CACnB,CAAC;;;ACSF,MAAM,kBAAkB,EAAE,OAAO;CAC/B,OAAO,EAAE,QAAQ,CAAC,KAAK,EACrB,aAAa,6BACd,CAAC;CACF,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,KAAK,EACtD,aAAa,8CACd,CAAC;CACH,CAAC;AAEF,MAAa,aAAoC,YAC/C,mBACE,mBACA;CACE,QAAQ;CACR,MAAM;CACN,UAAU,EACR,QAAQ,EACN,MAAM,EAAE,EAIT,EACF;CACF,EACD,OAAO,QAAQ;CACb,MAAM,EAAE,UAAU,IAAI;AAGtB,KAAI,CADiB,EAAE,OAAO,CAAC,UAAU,MAAM,CAC7B,QAChB,OAAM,SAAS,KAAK,eAAe,iBAAiB,cAAc;CAGpE,MAAM,WAAW,iBACf,SACA,IAAI,KAAK,UACT,IAAI,QAAQ,OACb;CAED,MAAM,kBAAkB,MAAM,aAAa;CAE3C,IAAI,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACjD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACF,CAAC;AAEF,KAAI,CAAC,KACH,KAAI;AACF,SAAO,MAAM,IAAI,QAAQ,QAAQ,OAA0B;GACzD,OAAO;GACP,MAAM;IACJ,OAAO;IACP,UAAU,WAAW,KAAK,UAAU,SAAS,GAAG,KAAA;IACjD;GACF,CAAC;UACK,GAAG;AACV,MAAI,QAAQ,OAAO,KAAK,sBAAsB;AAC9C,SAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;GAC7C,OAAO;GACP,OAAO,CACL;IACE,OAAO;IACP,OAAO;IACR,CACF;GACF,CAAC;;AAIN,KAAI,QAAQ,yBAAyB,QAAQ,CAAC,KAAK,eAAe;EAChE,MAAM,QAAQ,MAAM,6BAClB,IAAI,QAAQ,QACZ,iBACA,KAAA,GACA,QAAQ,aAAa,KACtB;EACD,MAAM,MAAM,GAAG,IAAI,QAAQ,QAAQ,qBAAqB;AAWxD,MATa,MAAM,QAAQ,sBACzB;GACE;GACA;GACA;GACD,EACD,IAAI,QACL,CAGC,OAAM,IAAI,QAAQ,QAAQ,OAAa;GACrC,OAAO;GACP,OAAO,CAAC;IAAE,OAAO;IAAS,OAAO;IAAiB,CAAC;GACnD,QAAQ,EAAE,yCAAyB,IAAI,MAAM,EAAE;GAChD,CAAC;;AAIN,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;EAEL;AAEH,MAAM,eAAe,EAAE,OAAO,EAC5B,OAAO,EAAE,QAAQ,CAAC,KAAK,EACrB,aAAa,iCACd,CAAC,EACH,CAAC;AAEF,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,OAAO;CACR,EACD,OAAO,QAAQ;CACb,MAAM,EAAE,UAAU,IAAI;CAEtB,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,UAAU,OAAO,IAAI,aAAa,CAAC,OAAO,IAAI,QAAQ,OAAO,EAAE,EACzE,YAAY,CAAC,QAAQ,EACtB,CAAC;UACK,GAAG;AACV,MAAI,aAAa,WACf,OAAM,SAAS,KAAK,gBAAgB,iBAAiB,cAAc;AAErE,QAAM,SAAS,KAAK,gBAAgB,iBAAiB,cAAc;;CAGrE,MAAM,SAAS,gBAAgB,MAAM,IAAI,QAAQ;CAEjD,IAAI,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACjD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO,OAAO;GACf,CACF;EACF,CAAC;AAEF,KAAI,CAAC,KACH,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;AAGJ,KAAI,KAAK,cACP,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;AAGJ,QAAO,MAAM,IAAI,QAAQ,QAAQ,OAAa;EAC5C,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO,OAAO;GACf,CACF;EACD,QAAQ,EACN,eAAe,MAChB;EACF,CAAC;AAEF,KAAI,CAAC,KACH,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;AAGJ,KAAI,QAAQ,gBACV,OAAM,IAAI,QAAQ,uBAAuB,QAAQ,gBAAgB,EAAE,MAAM,EAAE,IAAI,QAAQ,CAAC;AAG1F,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;EAEL;AAEH,MAAM,oBAAoB,EAAE,OAAO,EACjC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAClB,aAAa,qCACd,CAAC,EACH,CAAC;AAEF,MAAa,eAAsC,YACjD,mBACE,qBACA;CACE,QAAQ;CACR,MAAM;CACP,EACD,OAAO,QAAQ;CACb,MAAM,EAAE,OAAO,IAAI;AAYnB,KAAI,CAVS,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACF,CAAC,CAGA,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;AAGJ,OAAM,IAAI,QAAQ,QAAQ,OAAO;EAC/B,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACF,CAAC;AAEF,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;EAEL;AAEH,MAAM,eAAe,EAAE,OAAO,EAC5B,OAAO,EAAE,QAAQ,CAAC,KAAK,EACrB,aAAa,qDACd,CAAC,EACH,CAAC;AAEF,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,MAAM;CACP,EACD,OAAO,QAAQ;CACb,MAAM,EAAE,UAAU,IAAI;AAGtB,KAAI,CADiB,EAAE,OAAO,CAAC,UAAU,MAAM,CAC7B,QAChB,OAAM,SAAS,KAAK,eAAe,iBAAiB,cAAc;CAGpE,MAAM,kBAAkB,MAAM,aAAa;CAE3C,MAAM,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACF,CAAC;AAEF,KAAI,CAAC,KACH,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;AAGJ,KAAI,QAAQ,yBAAyB,QAAQ,CAAC,KAAK,eAAe;EAChE,MAAM,QAAQ,MAAM,6BAClB,IAAI,QAAQ,QACZ,iBACA,KAAA,GACA,QAAQ,aAAa,KACtB;EACD,MAAM,MAAM,GAAG,IAAI,QAAQ,QAAQ,qBAAqB;AAWxD,MATa,MAAM,QAAQ,sBACzB;GACE;GACA;GACA;GACD,EACD,IAAI,QACL,CAGC,OAAM,IAAI,QAAQ,QAAQ,OAAa;GACrC,OAAO;GACP,OAAO,CAAC;IAAE,OAAO;IAAS,OAAO;IAAiB,CAAC;GACnD,QAAQ,EAAE,yCAAyB,IAAI,MAAM,EAAE;GAChD,CAAC;;AAIN,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;EAEL;AAEH,MAAM,eAAe,EAAE,OAAO;CAC5B,IAAI,EAAE,QAAQ,CAAC,KAAK,EAClB,aAAa,gCACd,CAAC;CACF,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,KAAK,EACtD,aAAa,8CACd,CAAC;CACH,CAAC;AAEF,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,MAAM;CACN,UAAU,EACR,QAAQ,EACN,MAAM,EAAE,EAIT,EACF;CACF,EACD,OAAO,QAAQ;CACb,MAAM,EAAE,OAAO,IAAI;AAYnB,KAAI,CAVS,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACF,CAAC,CAGA,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;CAGJ,MAAM,WAAW,iBACf,SACA,IAAI,KAAK,UACT,IAAI,QAAQ,OACb;AAED,OAAM,IAAI,QAAQ,QAAQ,OAAa;EACrC,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;GACR,CACF;EACD,QAAQ,EACN,UAAU,WAAW,KAAK,UAAU,SAAS,GAAG,KAAA,GACjD;EACF,CAAC;AAEF,QAAO,IAAI,KAAK,EACd,QAAQ,MACT,CAAC;EAEL;AAEH,SAAS,iBACP,SACA,UACA,QACA;AACA,KAAI,CAAC,YAAY,CAAC,QAAQ,UAAU,iBAClC,QAAO;CAET,MAAM,mBAAmB,QAAQ,SAAS,iBAAiB,aAAa,SAAS,SAAS;AAE1F,KAAI,4BAA4B,QAC9B,OAAM,SAAS,KAAK,yBAAyB,iBAAiB,+BAA+B;AAG/F,KAAI,iBAAiB,QAAQ;AAC3B,SAAO,MAAM,oBAAoB,iBAAiB,OAAO;AACzD,QAAM,SAAS,KAAK,eAAe,iBAAiB,iBAAiB;;AAGvE,QAAO,iBAAiB;;;;ACrZ1B,MAAaA,SAAO,EAClB,MAAM,EACJ,QAAQ;CACN,WAAW;EACT,MAAM;EACN,oCAAoB,IAAI,MAAM;EAC9B,UAAU;EACV,OAAO;EACR;CACD,WAAW;EACT,MAAM;EACN,oCAAoB,IAAI,MAAM;EAC9B,gCAAgB,IAAI,MAAM;EAC1B,UAAU;EACV,OAAO;EACR;CACD,OAAO;EACL,MAAM;EACN,UAAU;EACV,QAAQ;EACT;CACD,eAAe;EACb,MAAM;EACN,cAAc;EACd,UAAU;EACV,OAAO;EACR;CACD,yBAAyB;EACvB,MAAM;EACN,UAAU;EACV,OAAO;EACR;CACD,UAAU;EACR,MAAM;EACN,UAAU;EACX;CACF,EACF,EACF;AAED,MAAa,aAAoC,YAAe;AAC9D,QAAO,YAAYA,QAAM,QAAQ,OAAO;;;;ACvC1C,MAAa,QAA+B,UAAa,EAAE,KAAU;AACnE,QAAO;EACL,IAAI;EACJ,QAAQ,UAAU,QAAQ;EAC1B,WAAW;GACT,WAAW,UAAU,QAAQ;GAC7B,QAAQ,OAAO,QAAQ;GACvB,aAAa,YAAY,QAAQ;GACjC,QAAQ,OAAO,QAAQ;GACvB,QAAQ,OAAO,QAAQ;GACxB;EACQ;EACT,WAAW,CACT;GACE,cAAc,SAAS,CAAC,mBAAmB,eAAe,CAAC,SAAS,KAAK;GACzE,QAAQ,QAAQ,WAAW,UAAU;GACrC,KAAK,QAAQ,WAAW,OAAO;GAChC,CACF;EACD,cAAc;EACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth-lead",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Better Auth Lead plugin",
5
5
  "homepage": "https://github.com/marcjulian/better-auth-plugins#readme",
6
6
  "bugs": {
@@ -1,12 +0,0 @@
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
@@ -1 +0,0 @@
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"}