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 +63 -26
- package/dist/client.d.mts +8 -10
- package/dist/client.mjs +1 -3
- package/dist/client.mjs.map +1 -1
- package/dist/{index-CGt_sc05.d.mts → index-E9vItimA.d.mts} +40 -9
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +40 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/error-codes-CZUPEOrE.mjs +0 -12
- package/dist/error-codes-CZUPEOrE.mjs.map +0 -1
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
|
-
- `
|
|
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 ({
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
188
|
+
validationSchema: leadMetadataSchema,
|
|
169
189
|
},
|
|
170
190
|
}),
|
|
171
191
|
],
|
|
172
192
|
});
|
|
173
193
|
```
|
|
174
194
|
|
|
175
|
-
|
|
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
|
|
184
|
-
|
|
|
185
|
-
| id
|
|
186
|
-
| email
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
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
|
|
197
|
-
createdAt
|
|
198
|
-
updatedAt
|
|
199
|
-
email
|
|
200
|
-
emailVerified
|
|
201
|
-
|
|
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-
|
|
2
|
-
import
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
package/dist/client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\n\nimport {
|
|
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
|
|
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
|
-
|
|
61
|
+
lead: Lead;
|
|
59
62
|
url: string;
|
|
60
63
|
token: string;
|
|
61
|
-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
|
52
|
-
|
|
57
|
+
if (await options.sendVerificationEmail({
|
|
58
|
+
lead,
|
|
53
59
|
url,
|
|
54
|
-
token
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
138
|
-
|
|
148
|
+
if (await options.sendVerificationEmail({
|
|
149
|
+
lead,
|
|
139
150
|
url,
|
|
140
|
-
token
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -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"}
|